一:定时器Timer
Timer是一种定时器工具,用来在一个后台线程计划执行指定任务;它可以计划执行一个任务一次或反复多次。
首先看下Timer定时执行的例子;我们用Timer实现三秒后输出hello,I have done it;代码如下所示:
public class Test2 {
static public void main(String... args){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello,I have done it");
}
},3000);
while (true){
// 此处打印每秒的时间,让结果更清晰
System.out.println(new Date().getSeconds());
try {
// 睡眠一秒,每秒打印一次当前的秒数
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 输出结果:
54
55
56
hello,I have done it
57
58
可以看到,在三秒之后输出了想要输出的语句;那么这个执行过程是什么样的呢?首先需要了解一下TimerTask类
/**
* A task that can be scheduled for one-time or repeated execution by a Timer.
*
* @author Josh Bloch
* @see Timer
* @since 1.3
*/
public abstract class TimerTask implements Runnable {
}
我们可以看到,TimerTask类是一个抽象类,并且实现了Runnale接口。显然,当我们在创建一个匿名内部类并重写了run方式时,就相当于创建了一个线程,并在run方法里面实现自己的逻辑。
再看Timer类的schedule方法,它是一个重载方法,查看源码如下所示:
// 等待多久后执行task,只执行一次
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
// 在某个时间执行task,只执行一次
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
// 等待多久之后执行task,并且每隔多久执行一次
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, -period);
}
// 第一次执行时间,之后每隔多久就再次执行
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
我们所用的就是过多久就执行,并只执行一次的那个方法;接下来我们再看个例子,延迟3秒后执行,并且执行之后每两秒执行一次,代码如下:
public class Test2 {
static public void main(String... args){
Timer timer = new Timer();
MyTimerTask myTimerTask = new MyTimerTask();
timer.schedule(myTimerTask,3000,2000);
while (true){
// 此处打印每秒的时间,让结果更清晰
System.out.println(new Date().getSeconds());
try {
// 睡眠一秒,每秒打印一次当前的秒数
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyTimerTask extends TimerTask{
@Override
public void run() {
System.out.println("you still have lots more to work on!");
}
}
- 输出结果如下:
0
1
2
you still have lots more to work on!
3
4
you still have lots more to work on!
5
6
you still have lots more to work on!
7
8
you still have lots more to work on!
9
可以清晰的看到:在三秒之后执行了一次,并且之后每过2s都再次执行。此处没有用匿名内部类,而是自己定义了一个类去继承抽象类TimerTask,效果都是一样的。
看到这儿,我们可以定义自己的需求去定时执行任务,比如;我们想要实现一个功能,首先是等两秒之后执行,然后再是等四秒执行,再是等两秒执行,如此交替反复执行;该如何实现该功能呢?代码如下:
public class Test2 {
static public void main(String... args){
Timer timer = new Timer();
MyTimerTask myTimerTask = new MyTimerTask();
timer.schedule(myTimerTask,2000);
while (true){
// 此处打印每秒的时间,让结果更清晰
System.out.println(new Date().getSeconds());
try {
// 睡眠一秒,每秒打印一次当前的秒数
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyTimerTask extends TimerTask{
private static int i = 0;
@Override
public void run() {
// 执行一次,数量加1
i++;
System.out.println("you still have lots more to work on!");
// 根据i的值来判断应该延迟多少s执行
new Timer().schedule(new MyTimerTask(),2000+(i % 2 == 0 ? 0 : 2000));
}
}
输出结果如下:
- 52
53
you still have lots more to work on!
54
55
56
57
you still have lots more to work on!
58
59
you still have lots more to work on!
0
1
2
3
you still have lots more to work on!
4
5
解析:因为需要两秒和四秒交替执行,所以用原本的方式是不能实现的,那么就应该继承TimerTask类来自己实现run方法的逻辑,我们在类中定义一个静态变量i,通过i的奇偶值来判断应该等待两秒还是等待四秒执行,并在run方法里面动态的传入需要等待的时间,以此来达到目的。
该程序执行时,首先我们第一次调用时传的2000,第一次输出时是在2s后,当输出完之后,再次调用timer对象的schedule方法,此时,传入的TimerTask是自己定义的MyTimerTask类,此时i的值为1,而传入delay值的表达式为:2000+(i % 2 == 0 ? 0 : 2000),1%2 不等于0;那么再次调用的时候,delay的值就是4000,然后再执行到这儿的时候,i的值为2,此时delay的值就为2000;如此交替执行下,就能实现想要达到的结果。
现在实现定时任务都是用开源框架quartz,它的cron表达式能够很好的定义时间去执行job
二:线程之间的通信
当多个线程一起工作时,我们希望它们能够有规律的执行,此时就要用到线程通信。
例如:我们想要实现两个线程同时输出(子线程和主线程),子线程输出2次,主线程输出4次,如此循环10次。那么应该怎么做呢?我们很容易想到定义两个线程来输出;代码如下:
public class Test2 {
static public void main(String... args){
final Work work = new Work();
// 子线程输出2次,循环10次
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1;i <=10; i++ ){
work.subSay(i);
}
}
}).start();
// 主线程输出4次,循环10次
for (int i = 1; i <= 10; i++){
work.mainSay(i);
}
}
}
class Work{
public void subSay(int j){
for (int i = 1; i <= 2; i++){
System.out.println(Thread.currentThread().getName()+"子线程输出第"+i+"次,第"+j+"轮");
}
}
public void mainSay(int j){
for (int i = 1; i <= 4; i++){
System.out.println(Thread.currentThread().getName()+"主线程输出第"+i+"次,第"+j+"轮");
}
}
}
我们很容易想到,将两个线程的输出操作封装到一个对象里,然后启动两个线程去调打印方法,如上图所示,该程序的执行结果并不能达到我们的要求,结果就不展示了,因为上面的程序执行,没有同步,没有通信,那么就是谁抢到cpu的执行权谁就执行,所以执行的顺序是凌乱的;接下来我们改造一下该Work类来达到我们的目的。
public class Test2 {
static public void main(String... args){
final Work work = new Work();
// 子线程输出2次,循环10次
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1;i <=10; i++ ){
work.subSay(i);
}
}
}).start();
// 主线程输出4次,循环10次
for (int i = 1; i <= 10; i++){
work.mainSay(i);
}
}
}
class Work{
/**
* 定义一个变量,当该值为true时,子线程打印,为false时,主线程打印
*/
private static boolean flag = true;
public synchronized void subSay(int j){
if (!flag){
try {
// 等待,让出cpu执行权
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 1; i <= 2; i++){
System.out.println(Thread.currentThread().getName()+"子线程输出第"+i+"次,第"+j+"轮");
}
// 自己执行完成之后改变状态
flag = false;
// 唤醒主线程
this.notify();
}
public synchronized void mainSay(int j){
if (flag){
try {
// 等待,让出cpu执行权
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 1; i <= 4; i++){
System.out.println(Thread.currentThread().getName()+"主线程输出第"+i+"次,第"+j+"轮");
}
// 改变状态让子线程打印
flag = true;
// 唤醒子线程
this.notify();
}
}
执行结果为:
- Thread-0子线程输出第1次,第1轮
Thread-0子线程输出第2次,第1轮
main主线程输出第1次,第1轮
main主线程输出第2次,第1轮
main主线程输出第3次,第1轮
main主线程输出第4次,第1轮
…
Thread-0子线程输出第1次,第10轮
Thread-0子线程输出第2次,第10轮
main主线程输出第1次,第10轮
main主线程输出第2次,第10轮
main主线程输出第3次,第10轮
main主线程输出第4次,第10轮
可以看到,两个线程现在就能完美协调的进行打印数据了;为什么呢?我来解释一下:
首先我们在两个方法都加上了synchronized,这样就会让子线程在执行打印方法时,主线程不会去执行主线程的打印方法;可能有人会问,我主线程又没有执行子线程的打印方法,为什么还要等待子线程执行完成呢?前面我们知道,synchronized是对象锁,而两个线程都是调用相同对象的方法,所以,当锁被子线程持有时,主线程就获取不到work对象的锁,就不会执行同步方法里面的代码。然后子线程进入方法之后会有两种情况:
- 此时该子线程执行,那么执行完之后释放锁,由主线程执行
- 不该子线程执行,那么调用wait()方法,释放锁,主线程执行完之后,唤醒子线程。
主线程依然如此,所以就会出现两个线程有序的进行工作。
代码中,我们判断flag状态时用的是if,这样是不好的,应该将if换成while,换成while的目的是为了防止虚假唤醒
虚假唤醒就是一些obj.wait()会在除了obj.notify()和obj.notifyAll()的其他情况被唤醒,而此时是不应该唤醒的。
介绍Object的两个方法:
- wait() 线程进入等待状态,释放锁,让出cpu的执行权
- notify() 唤醒一个在等待的线程
注意:这两个方法必须在同步代码块或者同步方法中才能够使用
三:notify 与notifyAll 的区别
notify只会随机通知一个在等待的对象,而notifyAll会通知所有在等待的对象,并且所有对象都会继续运行
- notify:当有多个线程在等待锁时,使用notify会随机唤醒一个线程,被唤醒的线程会获得对象锁,然后继续运行
- notifyAll:当有多个线程在等待锁时,使用notifyAll会唤醒所有等待的线程,然后被唤醒的线程些就会去争抢锁,谁抢到了锁,谁就执行;没有抢到锁的线程会进到线程的锁池中。
有兴趣的小伙伴可以试试实现一个线程不断制造面包,另外五个线程去消费面包,当有面包是才能去消费。
四:wait与sleep的区别
当某个线程持有某个对象的锁时,如果使用wait方法会释放该对象的锁,将cpu的执行权让给其他线程去执行。
而sleep则不会释放线程持有所持有对象的锁。