Java并发编程实战——详解Condition的await和signal等待通知机制

wait()、notify()为什么在Ojbect类下?

在jdk1.5以前,我们使用synchronized,而因为synchronized中的这把锁可以是任意对象的,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object。

关于wait()暂停的是持有锁的对象,所以想调用wait()必须为:对象.wait();
notify()唤醒的是等待锁的对象,调用:对象.notify();
可以通过下面的代码理解。

Object obj = newObject();
synchronized(obj){
    try{  
      obj.wait();
      }catch(Exception e){}
      obj.notify();
  }

所以说,因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在object类中。

在jdk1.5以后,将同步synchronized替换成了Lock,将同步锁对象换成了Condition对象,并且Condition对象可以有多个,这样可以解决一个问题:比如说我们在多个生产者和消费者模式中:

boolean flag = false;
public synchronized void set(String name){
  while(flag){//用while而不用if的原因,这样每个线程在wait等待醒来后都必须再次判断flag
    try{this.wait();}catch(Exception e){}  
  }
  Sytem.out.printLn("生产者");
  flag = true;
  this.notifyAll();//这将唤醒所有线程(本方线程和对方线程),消耗资源
}
public synchronized void out(){
  whie(!flag){
     try{this.wait();}catch(Exception e){}
  }
  Sytem.out.printLn("消费者");
  flag = false;
  this.notifyAll();//这将唤醒所有线程(本方线程和对方线程),消耗资源
}

上面的做法很消耗资源,如果把notifyAll()改成notify()的话,就会造成可能所有线程都在等待。

Condition初探

所以在jdk1.5以后提供了Lock接口和Condition对象。Condition中的await(), signal().signalAll()代替Object中的wait(),notify(),notifyAll()

private Lock lock = new ReentrantLock();
private Condition condition_pro = lock.newCondition();//生产者对象
private Condition condition_con = lock.newCondition();//消费者对象
public void set(String name) throws Exception{
  lock.lock();//加锁
  try{
    while(flag){
     	contion_pro.await();
     }
    Sytem.out.printLn("生产者");
    flag= true;
    condition_con.singal();//指定唤醒消费方
  }finally{
    lock.unlock();//解锁    
  }  
}
public void out() throws Exception{
  lock.lock();
	try{
		while(!flag){
			condition_con.await(); 
    	}
     Sytem.out.printLn("消费者");
     flag = false;
     condition_pro.signal();//指定唤醒生产方
   }finally{
      lock.unlock();  
   }
}

这样做的好处,我们可以指定唤醒某一方,减少消耗。

从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持

参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:

  • void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
  • long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;
  • boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
  • boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间
  • void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
  • void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

Condition实现原理分析

注意到ConditionObject中有两个成员变量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列,所以condition的实现原理其实与AQS的同步队列非常相似。这里直接用图来表示。

注意:这里同步队列是AQS拥有的,等待队列是Condition拥有的,他们的节点都保存了线程相关的信息。

在这里插入图片描述

await()的实现:

为了介绍等待队列,首先来看下面一段代码:

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            lock.lock();
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });
        thread.start();
    }
}

这段代码没有任何实际意义,甚至很臭,只是想说明下我们刚才所想的。新建了10个线程,没有线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点,debug模式下情景图如下:
在这里插入图片描述
从上图我们可以很清楚的看到这样几点:1. 调用condition.await方法后线程依次尾插入到等待队列中,如图队列中的线程引用依次为Thread-0,Thread-1,Thread-2….Thread-8;2. 等待队列是一个单向队列。
如图所示:
在这里插入图片描述

同时还有一点需要注意的是:我们可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。示意图如下:

在这里插入图片描述

await()方法实现过程如下:
在这里插入图片描述
由上图所示:
调用condition.await方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的Node尾插入到等待队列中。

sinal()方法

调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值