如何理解Condition

在JDK1.5以后并发包中提供了锁定接口,条件接口与锁配合使用可以实现等待/通知模式,在此之前是使用定义在对象对象上的一组监视器方法,主要包括:等待() wait(long timeout),notify()以及notifyAll()方法,这些方法同步结合使用,也可以实现等待/通知。


对象的监视器方法与条件接口的对比如下(图片截取自Java的并发编程的艺术)

条件演示

public class ConditionTest {
   Lock lock = new ReentrantLock();
   Condition condition = lock.newCondition();

   public void conditionWait() throws InterruptedException {
       lock.lock();
       try {
           System.out.println(Thread.currentThread());
           condition.await();
           System.out.println("await");
       } finally {
           lock.unlock();
       }
   }

   public void conditionSignal() throws InterruptedException {
       lock.lock();
       try {
           System.out.println(Thread.currentThread());
           condition.signal();
           System.out.println("siganl");
       } finally {
           lock.unlock();
       }
   }


   public static void main(String[] args) throws InterruptedException {
       ConditionTest conditionTest = new ConditionTest();
       new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   conditionTest.conditionWait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }) {
           public String toString() {
               return getName();
           }
       }.start();

       new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   conditionTest.conditionSignal();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }) {
           public String toString() {
               return getName();
           }
       }.start();
   }
}

运行结果:

从上面的演示比较容易得出:

1,一般都会将条件对象作为成员变量。

2,线程1调用等待()方法后当前线程会释放锁并等待。

3,线程线程1调用信号()方法通知线程0,线程0从等待()返回,在返回前已经获取到了锁。


上面demo中lock.newCondition()其实返回的是Condition的一个实现:AQS中的ConditionObject

public class ConditionObject implements Condition, java.io.Serializable {
   private static final long serialVersionUID = 1173984872572414699L;
   //这里Condition维护了自己的等待队列
   /** First node of condition queue. */
   private transient Node firstWaiter;
   /** Last node of condition queue. */
   private transient Node lastWaiter;

  ...省略后续的代码...


一个的ConditionObject包含一个等待队列,的ConditionObject包括首节点和尾节点,等待队列是一个FIFO队列,如果一个线程调用Condition.await()方法,那么该线程将会释放锁,构造成节点加入等待队列并进入等待状态,并将该节点从尾部加入等待队列。节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。

等待队列的基本结构如图,同步队列的结构图可以参见AQS与简介源码分析



条件接口提供了如下方法:

public interface Condition {

   //当前线程进入等待直到被通知或者中断
   void await() throws InterruptedException;

   //跟上面的区别是对中断不敏感
   void awaitUninterruptibly();

   //当前线程进入等待状态直到被通知,返回值表示剩余时间,如果在nanosTimeout纳秒之前被唤醒
   //那么返回值就是nanosTimeout-实际时间,如果返回值是0或者是负数表示已经超时
   long awaitNanos(long nanosTimeout) throws InterruptedException;

   //当前线程进入等待状态直到被通知,中断或者经过指定的等待时间,在等待时间之前被唤醒返回true,时间到了则返回false,可以自定义超时时间的单位
   boolean await(long time, TimeUnit unit) throws InterruptedException;

   //当前线程进入等待状态直到被通知,中断或者到某个时间,如果没有到指定时间就被通知,返回true,否则到达指定时间返回false
   boolean awaitUntil(Date deadline) throws InterruptedException;

   //唤醒一个等待在Condition上的线程,该方法从等待方法返回前必须获得与Contion相关联的锁
   void signal();

   //唤醒所有等待在Condition上的线程,能够从等待方法返回前必须获得与Contion相关联的锁
   void signalAll();
}


等待

调用条件的等待()方法(或者以AWAIT开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从AWAIT()方法返回时,当前线程一定获取了条件相关联的锁。

如果从队列(同步队列和等待队列)的角度看AWAIT()方法,当调用AWAIT()方法时,相当于同步队列的首节点(获取了锁的节点)移动到条件的等待队列中。

public final void await() throws InterruptedException {
   //如果线程被中断则抛出异常
   if (Thread.interrupted())
       throw new InterruptedException();
   //将当前线程构造成节点加入到等待队列节点中
   Node node = addConditionWaiter();
   //释放当前线程占有的锁
   int savedState = fullyRelease(node);
   int interruptMode = 0;
   //判断当前节点是否在AQS队列中,如果不在就继续挂起
   while (!isOnSyncQueue(node)) {
       LockSupport.park(this);
       //如果中断则退出循环
       if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
           break;
   }
   //竞争尝试获取锁,如果没有获取到则继续阻塞等待被唤醒再次竞争锁
   if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
       interruptMode = REINTERRUPT;
   //清理Contidion维护的等待队列
   if (node.nextWaiter != null) // clean up if cancelled
       unlinkCancelledWaiters();
   if (interruptMode != 0)
       reportInterruptAfterWait(interruptMode);
}


final boolean isOnSyncQueue(Node node) {
   //如果node节点的状态为CONDITION,表示节点状态正常没有被唤醒,此刻节点还没有进入AQS队列中
   if (node.waitStatus == Node.CONDITION || node.prev == null)
       return false;
   //如果当前节点的下一个节点不为空的话,那么该节点肯定在AQS队列中
   if (node.next != null) // If has successor, it must be on queue
       return true;
   //循环遍历,返回ture表示当前节点在AQS对列中
   return findNodeFromTail(node);
}


通知

调用条件的信号()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

public final void signal() {
   //判断当前线程是否为拥有锁的独占式线程,如果不是则抛出异常
   if (!isHeldExclusively())
       throw new IllegalMonitorStateException();
   //排队唤醒,最开始唤醒Condition维护的队列中的头节点
   Node first = firstWaiter;
   if (first != null)
       doSignal(first);
}


private void doSignal(Node first) {
   do {
       //如果头节点的下一个节点为null,则将头节点和其尾节点置null
       if ( (firstWaiter = first.nextWaiter) == null)
           lastWaiter = null;
       first.nextWaiter = null;
   } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}


final boolean transferForSignal(Node node) {

   //cas设置node节点从contion状态改为初始
   if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
       return false;
   //通过自旋的方式将节点加入到AQS队列中
   //等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
   Node p = enq(node);
   int ws = p.waitStatus;
   //如果当前节点的上个节点的waitStatus大于1则说明该节点所对应的线程等待超时或者被中断需要从同步队列中取消等待,如果CAS修改失败则直接唤醒
   //if语句一般走不到这里,唤醒的操作一般在lock.unlock()中
   if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
       LockSupport.unpark(node.thread);
   return true;
}


总结

演示中的具体流程如下:

1,线程1调用lock.lock()获取到锁,内部调用acquireQueued方法,线程被加入到AQS的同步队列中。

2,线程1调用AWAIT方法时,锁释放,该线程锁构成的节点从AQS的同步队列中移除,通过addConditionWaiter()方法加入到条件的等待队列中,等待着被通知的信号。

3,线程1释放锁唤醒胎面-2获取到锁,加入到AQS的同步队列中,处理业务

4,Tread-2调用signal方法,Condition的等待队列中只有Thread-1一个节点,通过调用enq(节点节点)方法加入到AQS的同步队列中,此时Thread-1并没有被唤醒,唤醒的操作是在finall块中的lock.unlock()中。

5,胎面2调用lock.unLock()方法,释放锁,线程1被唤醒并获取到锁从AWAIT()方法返回继续处理业务。

如图6所示,线程1调用解锁释放锁,结束整个流程。


参考文章:

Doug Lea:“Java并发编程实战”

方腾飞,魏鹏,程晓明:“并发编程的艺术”


优快云文章同步会慢些,欢迎关注微信公众号:挨踢男孩


    



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值