在前面几篇文章中,已经讲过了join()
方法的使用,我们知道它是用来控制线程的执行顺序的。
本篇文章中要讲到的wait()
方法和notify()
方法是用来控制线程执行顺序的,相比于join()
,它能够更精确地控制线程之间的执行顺序,接下来我们就来体会一下。
1. wait() & notify() 简介
wait()
叫做等待,调用wait()
的线程,就会进入阻塞的状态(WAITING);notify()
叫作唤醒,通过该方法可以唤醒调用了wait()
方法而进入阻塞的线程(由WAITNG变为RUNNABLE)
注意事项:这两个方法都是Object
类的成员方法,也就是说:当线程t1调用了o1.wait()
方法进入阻塞态后,只有使用o1.notify()
才可以将t1由阻塞态唤醒为就绪态。
2. wait() & notify() 使用
wait()
方法内部的执行过程分为以下几步:
- 释放锁
- 等待通知
- 当通知到达后,就会被唤醒,并且尝试重新获取锁(参与锁竞争)
2.1 wait()的使用
注意事项:wait()
的第一步上来就要释放锁,也就意味着:wait()
必须在synchronized
中使用,并且synchronized
的锁对象与调用wait()
的对象是同一个。
如果不在synchronized
中使用wait()
会出现什么问题呢?
public class Demo15 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait之前");
obj.wait();
System.out.println("wait之后");
}
}
运行结果:我们发现当程序调用到wait()
的时候抛了个异常
wait之前
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at thread.Demo15.main(Demo15.java:14)
异常的名称为IllegalMonitorStateException
,意思就是非法的监视器状态异常,所谓监视器指的就是synchronized(监视器锁)
因此我们需要给这段代码块加锁:
public class Demo15 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
synchronized (obj) {
System.out.println("wait之前");
obj.wait();
System.out.println("wait之后");
}
}
}
运行结果:
wait之前
我们发现程序运行到obj.wait();
之后就不继续往下走了,原因是wait()
的调用导致main线程被阻塞,由于没有被唤醒,它将一直处于阻塞状态,不参与调度。
通过jconsole
也可以查看到main线程当前的状态(WAITING):
要想让该线程被唤醒参与调度,只能通过notify()
来实现,接下来就来讲解一下notify()
方法。
2.2 notify()的使用
注意事项:
- 调用
notify()
的地方,也得使用synchronized
加锁,并且锁对象与调用wait()
处的锁对象相同; - 调用
wait()
的对象必须和调用notify()
的对象是同一个; - 当有多个线程等待的时候,
notify()
是随机唤醒一个线程,notifyAll()
则是唤醒所有线程共同竞争锁。
先定义一个用于等待的线程:
public class Demo16 {
//创建一个对象作为锁对象
public static void main(String[] args) {
Object locker = new Object();
//创建等待线程
Thread waitThread = new Thread(() -> {
synchronized (locker) {
System.out.println("wait开始");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait结束");
}
});
在定义一个用于唤醒的线程,并创建唤醒与等待线程:
//创建一个用来通知的线程
Thread notifyThread = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入任意内容,开始通知");
scanner.next();
synchronized (locker) {
System.out.println("notify开始");
locker.notify();
System.out.println("notify结束");
}
});
waitThread.start();
notifyThread.start();
}
}
运行结果:
wait开始
请输入任意内容,开始通知
1
notify开始
notify结束
wait结束
Process finished with exit code 0
2.3 使用场景
这里举两例子,来让大家更好地理解wait()
和notify()
,应该在什么时候使用。
2.3.1 两个线程有一定时序需求
- 线程1:需要计算一个结果
- 线程2:需要使用这个结果来完成其他事
线程的执行顺序一定是先执行线程1中计算的代码,才执行线程2中的代码。此时线程2就可以wait()
线程1,直到线程1计算出结果再notify()
,唤醒线程2。
2.3.2 防止线程饿死
由于系统的随机调度,可能导致某个线程迟迟不被调度上CPU,从而导致线程饿死。使用wait()、notify()可以在一定程度上防止线程饿死。
给大家举个例子,有三个线程t1、t2、t3,他们都是完成去银行取钱的操作;一个线程t4是去完成将钱存入银行的操作。
但是银行的ATM此时又没有钱,取钱者就算上CPU去执行取钱的操作也就没有什么实质的进展。
但是他们依然会被系统随机调度上CPU去运行。但由于某些取钱者的某些特性,可能导致存钱者的优先级比取钱者低,然后迟迟无法上CPU运行。取钱者此时又没有什么实质的进展,存钱者这个线程又一直无法分配到CPU资源。
但是如果将三个取钱线程都加上wait()进入阻塞队列等待,直到存钱者notify()才被唤醒,进入就绪队列等待调度,结果就大不一样了。
取钱者在依次被调度上CPU后,执行到wait()代码,就依次进入了阻塞队列,此时就只有一个取钱者的线程参与调度:
这样就能保证存钱者线程上CPU上执行完存钱的操作后,取钱者线程被唤醒,进入就绪队列等待,有效避免了线程饿死。
3. Java中三种阻塞状态的唤醒方式
下面的几种状态都会导致TCB进入阻塞队列不参与调度,它们的唤醒方式又有所不同。
- WAITING: 需要其他线程来唤醒
- BLOCKED: 在其他线程把锁释放后,由操作系统唤醒
- TIMED_WAITING: 操作系统会计时,时间到了由操作系统唤醒