本文主要是文字描述,如果有耐心则坚持看完,理解了必有收获~~
基于使用 synchronized 锁,JVM都会为锁对象维护两个集合,你必须知道的两个概念:
Entry Set(别名锁池):线程获取对象锁失败,则会进入这个对象的锁池。
(若线程A获取了对象锁,那么线程B再来获取这个对象锁,必定失败,B线程则会进入锁池)
Wait Set(别名等待池):线程获取锁后,调用了wait方法,那么就会进入这个对象的等待池。
(若线程A获取了对象锁,那么线程A调用了wait方法,A会释放锁,然后进入等待池)
再来看这三个方法,这三个方法必须放在synchronized代码块或者方法块中使用,否则会抛出:java.lang.IllegalMonitorStateException 异常
wait:调用这个方法,线程就会释放锁,然后进入等待池,必须等待别的线程唤醒自己。
和sleep不一样,1是sleep不会释放锁,2是sleep会在睡眠指定时间后被唤醒。
notify:调用这个方法,则会环境等待池中的一个线程
Note:被唤醒的线程不会立马执行,它只是从blocked -> runnable状态,形象的说是这个
被唤醒的线程从等待池中进入到锁池,锁池中的线程会去竞争该对象锁,获取到了才能执行
notifyAll:唤醒等待池中的所有线程,这是和 notify 唯一的区别
再来看两个常被提到的问题:
1 wait 要放在while里面循环,不要放在if语句中,否则不安全,可能出现死锁。
2 唤醒最好调用notifyAll,不要使用notify,否者不安全,可能出现死锁。
这两个说法并没有错,但是前提是多生产者和多消费者的情况下才是完全正确,但是如是一个生产者线程和一个消费者线程,那么问题1中也是可以使用if语句的,2中也是可以使用notify。
多消费者-多生产者下为什么不要在if语句里面使用wait()?
如果只有一个生产者线程,一个消费者线程,那其实是可以用if代替while的,因为线程调度的行为是开发者可以预测的,生产者线程只有可能被消费者线程唤醒,反之亦然,因此被唤醒时条件始终满足,程序不会出错。
但是在多消费者-多生产者下,wait()的线程永远不能确定其他线程会在什么状态下notify(),所以必须在被唤醒、抢占到锁并且从wait()方法退出的时候再次进行指定条件的判断,以决定是满足条件往下执行呢还是不满足条件再次wait()
多消费者-多生产者下为什么要用notifyAll?
两个生产者两个消费者的场景,如果我们代码中使用了notify()而非notifyAll(),假设消费者线程1拿到了锁,判断资源为空,那么wait(),释放锁;然后消费者2拿到了锁,同样资源为空,wait(),也就是说此时Wait Set中有两个线程;然后生产者1拿到锁,生产,有资源了,notify()了,那么可能消费者1被唤醒了,但是此时还有另一个线程生产者2在Entry Set中盼望着锁,并且最终抢占到了锁,但因为此时资源是有的,因此它要wait();然后消费者1拿到了锁,消费,notify();这时就有问题了,此时生产者2和消费者2都在Wait Set中,资源为空,如果唤醒生产者2,没毛病;但如果唤醒了消费者2,因为资源为空,它会再次wait(),这就尴尬了,万一生产者1已经退出不再生产了,没有其他线程在竞争锁了,只有生产者2和消费者2在Wait Set中互相等待,那传说中的死锁就发生了。
最后贴一个图看看demo吧
这是一个生产者和一个消费者,如果是多个生产设和多个消费者,直接将 notify() 改为 notifyAll() 即可
补充:
对于notify的问题,如果使用AQS里面的ReentrantLock来解决
ReentrantLock reentrantLock = new ReentrantLock(); Condition condition1 = reentrantLock.newCondition(); Condition condition2 = reentrantLock.newCondition();
这个时候就可以让生产者使用 condition1,消费者使用 condition2 来解决这个问题
Condition 类其实可以理解Object.wait 和 Object.notify 方法的封装类,只不过同一个ReentrantLock下的Condition是共享一个锁