为什么wait()方法必须在synchronized保护的同步代码中使用
注释中可以看到
The recommended approach to waiting is to check the condition being awaited in a {@code while} loop around the call to {@code wait}
The current thread must own this object’s monitor lock.
Lost Wake-Up Problem无法唤醒
什么意思
线程A调用object对象的 wait() 方法进入阻塞状态,接下来 没有其他线程 调用或者 其他线程都是提前调用 了object对象的 notify() 或者 notifyAll() 方法,这样线程A就会一直阻塞下去
举个例子
1 | class BlockingQueue { |
对于上面消费者生产者的例子,当消费者线程进入了while代码段,但是调用 wait() 之前由于线程调度暂停了,生产者线程此时执行了整个 give() 方法,等到消费者线程恢复的时候,再调用 wait() ,就会一直等待。陷入无穷唤醒
这个现象的根本问题就是对于消费者线程来说, buffer.isEmpty() 和 wait() 并不是原子操作,在中间被打断了,是线程不安全的。
所以说需要一个机制使得其他线程不会在判断-等待的间隙影响消费者线程
解决办法
随意加锁可行吗
定义一把锁 Lock lock = new Lock()
1 | class BlockingQueue { |
当消费者加锁进入,调用了 wait() 方法后并不会释放lock锁,然后生产者就会一直阻塞在第8行 lock.lock() ,注意,生产者线程阻塞在这里的时候其实是处于waiting状态
应该加什么锁?
我们需要一种机制能够让消费者调用wait的时候还能够释放其持有的锁,
只有BlockingQueue的对象锁才能够避免这个问题,也就是把give和take的操作放到synchronized同步块中
1 | class BlockingQueue { |
上述例子中,消费者进入waiting状态后,生产者进入synchronized代码块,执行了notifyAll/notify方法后,并不会立马唤醒消费者线程(MESA管程),只是通知消费者线程可以去竞争管程锁了,然后自己继续执行
在调用 obj.wait() 前必须要拿到当前obj对象的管程monitor对象,也就是obj的锁,这样在调用 obj.wait() 的时候就可以释放obj锁。如果锁的不是正确的对象,那么Java就会抛出 IllegalMonitorStateException 异常。
wait方法为什么要在loop中
MESA管程
这是MESA管程独有的
1 |
|
Hasen 模型、Hoare 模型和 MESA 模型的一个核心区别就是当条件满足后,如何通知相关线程。
管程要求同一时刻只允许一个线程执行,那当线程T2的操作使线程T1等待的条件满足时,A和B究竟谁可以执行呢?
- Hasen 模型,要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1 再执行,这样就能保证同一时刻只有一个线程执行。
- Hoare 模型,T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。
- MESA 管程,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,从入口队列进入到条件变量的等待队列,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量
虚假唤醒spurious wakeup
线程可能在没有被notify/notifyaAll也没有被中断或者超时的情况下被唤醒,这个概率非常小,但我们还是需要保证在发生虚假唤醒时候的正确性
notify和notifyAll方法什么时候使用
除非经过深思熟虑,否则尽量使用 notifyAll()
synchronized是非公平锁,
当我们使用 notifyAll() 时,它将唤醒所有的 wait 队列线程争夺锁,假如线程一个线程获取到了synchronized锁,但是不满足while的条件再次进入wait 时,其他唤醒的 wait 队列的线程仍旧会去争夺锁
想要使用notify需要满足下面三个条件
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行相同的操作;
- 只需要唤醒一个线程。
主要还是为了避免发生死等现象
为什么wait/notify/notifyAll在Object中,而sleep在Thread类中
wait/notify/notifyAll是锁级别的操作,管程monitor是对象级别的,所以在Object类中最合适
一个线程可能持有多把锁,如果wait方法在线程中,那么如何让一个线程持有多把锁呢,如何明确线程等待的是哪把锁呢?既然是让线程去等待某个对象的锁,所以自然应该通过操作对象来实现而不是操作线程
sleep和wait方法的异同
都让线程进入waiting或者timed waiting方法中
都可以响应interrupt中断,抛出
InterruptedExceptionwait会主动释放monitor锁,sleep不会释放monitor锁
sleep必须定义一个超时时间
wait必须在拿到对象的管程锁后才能使用,wait需要放在loop里面
Reference
https://juejin.cn/post/6939685932207439909
https://juejin.cn/post/6844904105811378189
https://time.geekbang.org/column/article/86089