用通知-等待机制优化锁等待问题

破坏占用且等待就可以避免死锁产生,以上一节中的循环等待代码来看:

// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
 ;

如果apply()操作耗时非常端,而且并发冲突量不大时,这个方案是不错的,因为这种场景下,循环上几次或者几十次就可以一次性获取锁,执行业务。但是如果apply()操作耗时长,或者并发冲突量很大的时候,循环等待就可能循环上万次才能获取锁,会导致CPU使用率升高,影响环境的问题。

这种场景,更好的解决方案是:如果线程要求的条件不满足,则线程阻塞自己,进入等待状态,在满足线程要求的条件后,通知等待的线程重新执行。

完整的等待-通知机制:线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。

用synchronized实现等待-通知机制

在Java语言中,使用synchronized配合wait()、notify()、notifyAll()这三个方法就可以轻松实现。

image

如图,左边一个等待队列,同一时刻,只允许一个线程进入synchronized保护的临界区,当有一个线程进入临界区后,其他线程只能进入图中左边的队列等待,这个等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列。

进入临界区后,由于不满足部分条件,需要进入等待状态,Java对象的wait()方法就能满足这个需求,当调用wait()方法后,当前线程就会被阻塞,并且进入右侧的等待队列,这个等待队列也是互斥锁的等待队列。线程在进入右侧等待队列的同时,会释放持有的互斥锁,线程释放后,其他线程就有机会获得锁,进入临界区。

当线程条件满足时,通知等待的线程,可以使用Java对象的notify()和notifyAll()方法,通知等待队列中的线程,条件曾经满足过,可以再次尝试获取锁,并判断条件是否满足。

对之前的代码进行优化

 while(条件不满足) {
   wait();
 }
class Allocator {
 private List<Object> als;
 // 一次性申请所有资源
 synchronized void apply(
   Object from, Object to){
   // 经典写法
   while(als.contains(from) ||
        als.contains(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();
 }
}

用notifyAll()而没有使用notify(),是因为notify()会随机通知等待队列中的一个线程,而notifyAll()会通知等待队列中所有的线程,第一反应是同一时间只有一个线程进入临界区,用notify()就可以了,实际上notify()可能导致某些线程永远不会被通知到。

学习来源:极客时间 《Java 并发编程实战》学习笔记 Day04

猜你喜欢

转载自blog.csdn.net/qq_38862257/article/details/128726822
今日推荐