1. wait 和 notify的作用及特点
1.1 两者都是Object类中的方法,都是 native 方法
两者都是Object
类中的方法,所以Java中所有的对象都可以调用这两个方法
public final native void notify();
public final native void wait(long timeout) throws InterruptedException;
并且从Java的源码中,我们可以知道:wait() notify() 都是native
方法
1.2 wait 都必须在 try catch中进行调用
Java规定**wait() 调用时,必须被 try catch 包围,而 notify() 不需要
**,
之所以 wait() 需要 try catc 的原因:
-
从上面的源码中,我们可以看到,在 wait() 中抛出了
InterruptedException
异常,所以我们必须在使用 wait() 时对该异常进行处理 -
notify()中没有抛出异常
,所以我们不需要进行处理
1.3 是否释放锁
- 调用
wait()时,会释放锁
,但是只会释放调用了 wait() 的锁,不会影响其他的锁 - 调用 notify() 时,不会释放锁
接下来,让我们用代码来进行验证是否释放锁
1. 对 wait() 来进行验证
public void run() {
super.run();
// 这里对 condition 来进行加锁
synchronized (condition){
try {
System.out.println("进入临界区");
condition.wait();
//因为释放了锁,所以不会输出下面的语句
System.out.println("调用了 wait() ,这里不会输出");
}catch (Exception e){
e.printStackTrace();
}
}
}
2. 对 notify() 来进行验证
public void run() {
super.run();
// 这里对 condition 来进行加锁
synchronized (condition){
System.out.println("进入临界区");
condition.notify();
//因为释放了锁,所以不会输出下面的语句
System.out.println("调用了 notify() ,这里仍然会进行输出");
}
}
分别运行上面的 代码,我们会发现调用了 wait() 以后的语句不会进行输出,而调用了 notify() 以后的语句仍然会进行输出,这个说明了 wait() 会释放锁, notify() 不会释放锁
从 notify() 不会释放锁中,我们可以得知
-
notify()最好尽量放在临界区代码的末尾
因为 notify() 不会释放锁,如果我们在临界区代码的中间或者前面调用了notify,会唤醒其他线程,而 notify 不会释放锁,从而导致那个线程可能有一段时间不会获得锁,发生
上下文切换
,导致性能不太好
1.4 生产者消费者
wait 和 notify
一般在消费者生产者中使用,消费者生产一般分为四种
- 单生产者单消费者
- 多生产者单消费者
- 单生产者多消费者
- 多生产者多消费者
但是这四种模式中 wait 和 notify 的使用都大同小异,比如有以下的注意点:
-
wait 和 notify 存放的东西在一个位置,一般我们使用阻塞队列,如 ArrayList,LinkedList 等
-
notify 放在消费线程的末尾
-
wait 的调用需要放在一个循环中,并且该循环需要在临界区中
循环的目的是:保证多生产者的情况下,阻塞队列不会超过最大容量
临界区的目的是:保证循环中的判断条件的修改是线程安全的
wait 的代码一般如下:
// 保证判断条件的修改是原子操作 atomic{ while(判断条件成立){ 调用 wait() } 执行生产操作 } // 通过 synchronized 来保证 synchorized(res){ // res 是生产者和消费者共享的 while(判断条件成立){ res.wait(); } 执行生产操作 }
2. 使用wait()和notify()时的注意事项
2.1 wait() 放在循环中
wait() 一般放在一个循环中,``循环的判断条件决定了是否调用 wait()`,
我们可以使用while 和 for
,但是一般来说我们使用while循环
如 1.4 的代码所示
循环一般放在临界区的前面部分
我们知道 wait() 和 notify() 最常用的场景是在生产者消费者的模式中,wait()
一般使用在消费者线程中,当判断条件成立时(如:生产的东西超过了可以存储的数量,消费者消费的速度跟不上生产的速度),这个时候我们就会把生产线程暂停,但是当我们唤醒生产线程时,它又会继续执行 wait() 以后的代码
并且,我们使用wait()的目的就是控制生产线程生产的速度,如果生产的代码不放在 wait() 后面,实际上不能达到控制速度的目的
为什么不使用 if,而使用循环
因为如果我们使用if,在多生产者的情景下,会出现错误
比如会出现这样的场景:
有3个生产者ABC,他们生产的东西放在一个有界队列中
A运行时队列满了,调用 wait() 暂停
然后在某一时间,消费线程唤醒了所有生产线程,但是A没有抢到锁,当A抢到锁时,BC生产的东西又把队列填满了,但是我们的wait()放在if中,这个时候因为队列满了,所以应该继续调用 wait() ,但是因为 if 只能使用一次,所以A只能继续执行生产代码,导致会出现异常
,如以下的代码
// 消费者和生产者在一个 ArrayList 中存取东西
// ArrayList 中最多可以存储 3 个,如果超过 3 个,会抛出异常
class Producer extends Thread {
ArrayList<Integer> list ;
public Producer (ArrayList<Integer> list){
this.list = list;
}
@Override
public void run() {
super.run();
while (true){
synchronized (list){
try {
// 这里使用 if
// 有可能有线程别唤醒时,size 还是等于3
if (list.size() >= 3){
list.wait();
}
list.add(1);
System.out.println("生产了一个");
// 如果存储超过 3 个,抛出异常
if (list.size() > 3) throw new Exception();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
class Consumer extends Thread{
ArrayList<Integer> list ;
public Consumer (ArrayList<Integer> list){
this.list = list;
}
@Override
public void run() {
while (true){
synchronized (list){
list.remove(0);
System.out.println("消费了一个");
list.notifyAll();
}
}
}
}
在多生产者的情况下运行上面的代码时,会发现有时候会抛出异常,而我们把 if改成了`while ,就不会报异常了
在单生产者的情况下,使用 if 和 while都不会抛出异常,但是我们不能保证这个类被使用时,是单生产者,还是多生产者
所以
- 在多生产者的情况下,一定使用循环
- 在单生产者的情况下,可以使用` if,但是因为不能保证使用该类时,一定是单生产者,所以还是建议使用循环
2.2 保证判断条件的线程安全
因为判断条件可能在多线程的情况下被修改,如多生产者或者多消费者的情况下,而在判断条件被修改的情况下,可能发生线程不安全的情况
一般来说,我们只要保证判断条件的修改是原子性的就行了
但是为了方便,一般我们就**把判断条件所在的循环放在临界区即可
**
如果不保证线程安全,可能会出现
IllegalMonitorStateException
异常
通常常见的情况就是:没有对 res 进行加锁(如没有 synchonized(res) ),res 就是消费者和生产者共享的资源
下面是一个正常的消费者线程的代码
class Produce extends Thread{
private Integer res ;
private boolean condition = true;
public Produce(Integer res){
this.res = res;
}
@Override
public void run() {
super.run();
synchronized (res){
try{
while (condition){
res.wait();
}
System.out.println("生产");
}catch (Exception e){
e.printStackTrace();
}
}
}
}
但是如果我们删掉 synchronized (res) ,那么就会报错
所以,如果出现了 IllegalMonitorStateException 异常,检查一下是否保证了线程安全
2.3 notify() 和 wait()在临界区的位置
-
notify() 放在临界区代码的末尾
因为 notify() 不会释放锁,如果放在前面,唤醒其他线程后,因为 notify() 所对应的锁没有释放,所以可能导致上下文切换
所以为了其他线程尽快得到锁,不发送上下文切换,我们尽可能放在后面
-
wait()所在的循环一般放在临界区的前面部分
因为 wait() 的目的是控制生产者线程生产的速度
如果 wait() 不放在生产代码的前面,那么就不能达到控制速度的目的,( wait() 不能影响生产代码,因为生产代码在调用 wait() 之前就执行了)
2.4 使用 notify 还是 notifyAll
我们要注意使用 notify 还是 notifyAll
- notify 随机唤醒一个线程
- notifyAll 唤醒所有的线程,但是 notifyAll 可能导致
过早唤醒,上下文切换
的问题
因为使用 notifyALl() 会出现一些问题,所以如果我们可以用 notify() 来完成,就不要使用notifyAll()
一般来说,如果满足下面的两个条件,那么我们可以使用 notify ,而不使用 notifyAll
-
一次通知最多需要唤醒一个线程
-
所有的等待线程都是相同的线程
这个可以理解为:
多生产者,如果都生产一个东西,那么不管唤醒哪个线程都可以满足消费者线程的需要
多生产者,不同的生产者线程生产不同的东西,假设为a,b,c,如果消费者线程需要 a ,但是 notify 随机唤醒的可能是 b,c,所以这个时候我们需要使用 notifyAll()
2.5 考虑使用 Condition
Condition
是Java中一个替代 wait notify
的一个库,他可以**解决过早唤醒的问题,并且解决了 wait()不能区分其返回是否是因为超时的问题
**
如果 wait notify 不能满足需要,可以考虑使用 Condition
3. 使用 notify 和 wait 遇到的一些问题
3.1 过早唤醒
这个问题常常出现在:有多个判断条件,但是都依赖于一个对象的 wait 和 notify
那么可以会出现一种情况:当消费线程使用 notifyAll() 来唤醒生产线程时,这个时候可以满足判断条件A,但是不满足判断条件B
,所以判断条件B所在的线程就被提前唤醒了
我们可以使用 Condition 来解决这个问题
我们可以使用 Condition 来解决这个问题,使用 Condition 时,我们可以为每一个判断条件设置一个 Condition
,调用 signal() 时,只会影响相应的线程,从而解决了过早唤醒的问题
但是注意:Condition只解决了过早唤醒的问题,没有解决信号丢失和上下文切换的问题
3.2 信号丢失
这种问题有两种情况
-
在不恰当的时候调用了 notify()
如可能有这种情况:
我们还没有调用 wait() ,但是已经调用了 notify() ,导致我们需要唤醒一个线程的时候,不能唤醒,因为我们已经调用过 notify() 了,这个时候因为等待线程没有收到唤醒线程的唤醒信号,所以这个信号就丢失了
这个问题,我们在
notify()外面嵌套一层循环即可
-
在应该使用 notify() 时,使用了 notifyAll()
比如:我们有A,B两个线程,我们这个时候想要唤醒A,但是 notify() 随机唤醒一个线程,如果唤醒了B,那么对于A来说,这个信号就相当于丢失了
我们可以看到,信号丢失实际上是代码层面的问题,不是Java自带的问题,所以只要我们写代码时注意就可以了
3.3 欺骗性唤醒
顾名思义,欺骗性唤醒就是没有 notify() 或 notifyAll() 的情况下唤醒了线程
有两种常见的情景:
-
非 InterruptedException
导致的问题这个是JVM中出现的问题,但是出现的频率很低
针对这种情况,我们只要将
判断条件 和 wait() ,放在临界区的一个循环中就行了
因为只有不满足判断条件,即使被欺骗唤醒,下一次循环还是会调用
wait()
-
InterruptedException
导致的问题调用了 interrupt() 但是没有进行处理,因为当线程处于
sleep() 或 wait()
时,如果我们调用 Interrupt(),那么会抛出java.lang.InterruptedException
异常,这个时候JVM会自动唤醒该线程针对这种情况,我们只要
在catch中,对InterruptedException进行处理即可
InterruptedException 导致欺骗性唤醒,如下面的代码
public class Main{
public static void main(String[] args) {
MyThread myThread = new MyThread(1);
myThread.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("main线程sleep完成");
myThread.interrupt();
}
}
}
class MyThread extends Thread{
Integer input;
public MyThread(Integer input){
this.input = input;
}
@Override
public void run() {
super.run();
synchronized (input){
while (true){
try {
System.out.println("这里调用wait()");
input.wait();
} catch (InterruptedException e) {
//这里没有对 InterruptException 进行处理,所以会导致问题
// e.printStackTrace();
}
System.out.println("这里调用了interrupt");
System.out.println("这里调用了没有调用notify,但是线程被唤醒了");
System.out.println("--------------------------------------");
}
}
}
}
那么我们会发现:我们没有调用 notify ,但是会发现没有调用,但是线程被唤醒了,每一秒会出现执行一次
3.4 上下文切换
上下文切换的问题就是因为多个线程不能及时的抢到锁导致的
上下文切换是一个JVM层面的问题,我们不能完全的解决,但是我们可以对他进行优化,有下面的两种方法
-
notify()能完成时,就不要使用 notifyAll()
-
notify() notifyAll() 放在临界区的末尾
因为 notify() 不会释放锁,所以我们为了不发生上下文切换,得让等待线程在被唤醒时,尽快获得锁,所以我们要让 notify() 唤醒时,尽快释放锁
3.5 锁死
wait 和 notify 导致的锁死是嵌套监视器锁死
的情况,即嵌套了多个锁,内部的锁一直不能被唤醒的
如以下的代码:
public class Main{
public static void main(String[] args) {
Integer outer = new Integer(1);
Integer res = new Integer(0);
Thread producer = new Producer(outer,res);
producer.start();
Thread consumer = new Consumer(outer,res);
consumer.start();
// 让主线程暂停三秒,然后查看消费者和生产者线程的状态
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("consumer线程的状态: "+consumer.getState());
System.out.println("producer线程的状态: "+producer.getState());
}
}
class Producer extends Thread{
private Integer outer ;
private Integer res;
public Producer (Integer outer,Integer res){
this.outer = outer;
this.res = res;
}
@Override
public void run (){
super.run();
synchronized (outer){
synchronized (res){
try {
//这里为了方便,直接设置为true
while (true){
System.out.println("producer线程调用wait()");
res.wait();
}
// 因为 condition 一直为true,所以不会运行到这里
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
class Consumer extends Thread{
private Integer outer ;
private Integer res;
public Consumer (Integer outer,Integer res){
this.outer = outer;
this.res = res;
}
@Override
public void run (){
System.out.println("消费者线程开始运行");
synchronized (outer) {
synchronized (res){
//在这个例子中,消费者线程是不会得到 outer 锁的
//所以 synchronized (outer) 里面的代码不会运行
System.out.println("消费者线程得到了 outer 锁");
System.out.println("消费");
res.notify();
}
}
}
}
上面代码的主要内容如下:
- 消费者和生产者内部都有两个变量 outer和res,并且在 run() 中都对这两个加锁,并且要
得到 outer 锁后才可以得到 res 锁
- 生产线程的判断条件设置为 true,所以生产线程第一时间调用 wait(),释放了 res 锁
- 主线程启动消费者和生产者线程后,沉睡3秒,然后查看两个线程的状态
运行后,发现结果如下
我们发现:
- 生产者线程调用了 wait() 后,消费者线程没有执行生产操作
- 两个线程最终都处于阻塞状态
经过分析,原因如下:
当前结果的原因是锁死,更确切地说是嵌套监视器锁死
造成的原因是因为 wait() 只能影响调用 wait 的对象锁,而在这里我们必须先得到外部锁,才可以继续申请 wait 的对象锁
生产者线程一直没有释放外部锁,导致消费者线程不能申请 wait 的对象锁,从而导致锁死