标签:关键点 文章 tps 互斥 wan 抛出异常 void 建议 sync
在前篇介绍死锁的文章中,我们破坏等待占用且等待条件时,用了一个死循环来获取两个账本对象。
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
??;
我们提到过,如果apply()操作耗时非常短,且并发冲突量也不大,这种方案还是可以。否则的话,就可能要循环上万次才可以获取锁,这样的话就太消耗CPU了!
于是我们给出另一个更好的解决方案,等待-通知机制:
若是线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程要求的条件满足时,通知等待的线程重新执行。
Java是支持这种等待-通知机制的,下面我们就来详细介绍这个机制,并用这个机制来优化我们的转账流程。
我们先通过一个就医流程来了解一个完善的“等待-通知”机制。
在医院就医的流程基本是如下这样:
我们将上述过程对应到线程的运行情况:
一个完整的“等待—通知”机制如下:
线程首先获取互斥锁,当线程要求条件不满足时,释放互斥锁,进入等待状态;当条件满足时,通知等待的线程,重新获取锁。
一定要理解每一个关键点,还需要注意,通知的时候虽然条件满足了,但是不代表该线程再次获取到锁时,条件还是满足的。
在Java中,等待—通知机制可以有多种实现,这里我们讲解由synchronized
配合wait()
、notify()
或者notifyAll()
的实现。
当线程进入获取锁进入同步代码块后,若是条件不满足,我们便调用wait()
方法使得当前线程被阻塞且释放锁。
上图中的等待队列和互斥锁是一一对应的,每个互斥锁都有自己的独立的等待队列(等待队列是同一个)。(这句话还在暗示我们后面唤醒线程时,是唤醒对应锁上的线程。)
当条件满足时,我们调用notify()
或者notifyAll()
,通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过。
我们要在相应的锁上使用wait() 、notify()和notifyAll()。
需要注意,这三个方法可以被调用的前提是我们已经获取到了相应的互斥锁。所以,我们会发现wait() 、notify() notifyAll()都是在synchronized{...}
内部中被调用的。如果在synchronized外部调用,JVM会抛出异常:java.lang.IllegalMonitorStateException。
我们现在使用“等待—通知”机制来优化上篇的一直循环获取锁的方案。首先我们要清楚如下如下四点:
使用“等待—通知”机制时,我们一般会套用一个“范式”,可以看作是前人的经验总结用法。
while(条件不满足) {
wait();
}
这个范式可以解决“条件曾将满足过”这个问题。因为当wait()返回时,条件已经发生变化,使用这种结构就可以检验条件是否还满足。
解决我们的转账问题:
class Allocator {
private List<Object> als;
// 一次性申请所有资源
synchronized void apply(Object from, Object to){
// 经典写法
while(als.contains(from) || als.contains(to)){
// from 或者 to账户被其他线程拥有
try{
wait(); // 条件不满足时阻塞当前线程
}catch(Exception e){
}???
}
als.add(from);
als.add(to);??
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll(); // 归还资源,唤醒其他所有线程
}
}
sleep()
和wait()
都可以使线程阻塞,但是它们还是有很大的区别:
wait()、notify()以及notifyAll()它们之间的联系是依靠互斥锁,也就同步锁(内置锁),我们前面介绍过,每个Java对象都可以用作一个实现同步的锁,所以这些方法是定义在Object中,而不是Thread中。
“等待—通知”机制是一种非常普遍的线程间协作的方式,我们在理解时可以利用生活中的例子去类似,就如上面的就医流程。上文中没有明显说明notify()和notifyAll()的区别,只是在图中标注了一下。我们建议尽量使用notifyAll(),notify() 是会随机地通知等待队列中的一个线程,在极端情况下可能会使某个线程一直处于阻塞状态不能去竞争获取锁导致线程“饥饿”;而 notifyAll() 会通知等待队列中的所有线程,即所有等待的线程都有机会去获取锁的使用权。
参考:
[1]极客时间专栏王宝令《Java并发编程实战》
[2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016
[3]skywang12345.Java多线程系列--“基础篇”05之 线程等待与唤醒.https://www.cnblogs.com/skywang12345/p/3479224.html
【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案
标签:关键点 文章 tps 互斥 wan 抛出异常 void 建议 sync
原文地址:https://www.cnblogs.com/myworld7/p/12231936.html