条件队列之Condition

158 阅读6分钟

前言

在Lock出现之前,我们使用对多的同步方式就是synchronized,配合Object的wait、notify来实现等待、通知机制。 Condition接口也提供了类似的方法,与lock配合使用。

介绍

Object的监视器方法:wait、notify、notifyAll,与Synchronized配合使用,必须先获得锁,才能执行监视器方法
Condition方法:await、signal、signalAll,与Lock配合使用,必须先获取Lock锁,才能执行Condition方法。

源码分析:

基本结构

通过UML类图可以看出,Condtion是一个接口,它的实现时在AbrtractQueuedSynchronizer的内部类ConditionObject实现的。下面主要从await和signal两个方法入手,从源码了解一下ContionObject.

ContionObject参数

  /** First node of condition queue. */
  private transient Node firstWaiter; // 等待队列首节点
  /** Last node of condition queue. */
  private transient Node lastWaiter; // 等待队列尾结点

await 方法

await会让当前线程挂起,直到别的线程打断,或者获取到锁的线程调用signal方法。
在await方法上挂起的线程,只有在以下情况才会唤醒:

  1. 被别的线程中断
  2. 其它获取锁的线程调用signal,且当前线程的节点处在首节点
  3. 其它获取锁的线程调用signalAll
  4. 发生虚假唤醒 在此方法返回之前,此线程必须重新获取到锁。

    现在来看AQS内部的实现逻辑:
public final void await() throws InterruptedException {
    if (Thread.interrupted()) // 检查是否中断
        throw new InterruptedException();
    Node node = addConditionWaiter(); // 添加节点到条件队列尾部
    int savedState = fullyRelease(node); // 完全释放锁,并把重入次数保存
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) { // 自旋判断该节点是否加入到同步队列
        LockSupport.park(this); // 如果不在同步队列,就挂起
        /*
            checkInterruptWhileWaiting 检查中断
            1 未发生中断返回0
            2 在调用signal/siganlall之前发生中断,返回THROW_IE(-1),并自己尝试加入到等待队列
            3 在调用signal/siganlall之后发生中断,返回REINTERRUPT(1)
            如果返回THROW_IE 在后面会抛出InterruptedException,如果返回REINTERRUPT 在后面会补中断信号
         *
         */
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    /*
        加入到同步队列的node,在acquireQueued中重新获取资源,如果在while中没有产生中断,在acquireQueued
        产生了中断,acquireQueued会返回true,仍把interruptMode=REINTERRUPT
     */
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    /*
    正常通过 singal/singalAll转移到同步队列的节点,node.nextWaiter为null
    如果线程产生中断THROW_IE或者fullyRelease 出现错误,node.nextWaiter 不为null
    那么需要清理点这些节点
     */
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters(); // 清理已经取消的节点
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode); // 如果interruptMode 为-1 抛出InterruptedException,如果为1 重新设置中断标识
}

await方法执行逻辑:

  1. 添加Node.CONDITION类型的节点,并加入到条件队列的尾部
  2. 完全释放锁,并保存锁的状态
  3. 判断有没有在同步队列,如果不在同队队列就挂起
  4. 从挂起处唤醒之后,检查是否发生了中断
  5. 从while循环跳出,在同步队列,准备获取锁 在介绍await执行流程的时候,提到了两个队列:
  6. ContinObject的队列,也叫条件队列
  7. AQS的队列,同步队列

addConditionWaiter

private Node addConditionWaiter() {
      Node t = lastWaiter; // 队列尾结点
      // If lastWaiter is cancelled, clean out.
      if (t != null && t.waitStatus != Node.CONDITION) { // 如果尾结点已经取消,则清理掉所有取消的节点
          unlinkCancelledWaiters(); // 清理所有已经取消的节点
          t = lastWaiter; // 从新获取尾结点
      }
      Node node = new Node(Thread.currentThread(), Node.CONDITION); // 创建一个Node.CONDITION类型的节点
      if (t == null) // 如果是第一次添加
          firstWaiter = node;
      else
          t.nextWaiter = node; // 挂到当前尾结点的后面
      lastWaiter = node; // 让lastWaiter指向自己
      return node;
  }

从addConditionWaiter方法可以看出,只是创建了一个类型为Node.CONDITION的节点。同时通过代码也可以看出:

  1. 条件队列只用到了Node中的Thread、waitStatus、nextWaiter
  2. 条件队列是单向队列 AQS队列和Contion队列的对比:
    AQS队列: Contion队列: waitStatus可能的几种状态说明:
  3. 默认是0
  4. 如果大于0,说明该节点超时或者中断,需要从队列中移除
  5. 如果等于-1,说明后继节点等待被唤醒
  6. 如果等于-2,说明在条件队列中
  7. 如果等于-3,说明唤醒状态需要向下传播,共享锁的时候会用到

fullyRelease(Node node)

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState(); // 获取状态
        if (release(savedState)) { // 释放锁,不管是重入几次,一次性释放所有的
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED; // 释放锁过程中发生异常,把当前节点设置成取消
    }
}

通过上面的分析我们已经清楚了,当获取到锁的线程调用await方法,会创建一个Node节点,并加入到条件队列的尾部,然后释放所持有的线程。如果不在同步队列中会被park,直到有别的线程signal。

isOnSyncQueue

final boolean isOnSyncQueue(Node node) {
  if (node.waitStatus == Node.CONDITION || node.prev == null) // 节点状态是Node.CONDITION或者前驱是null,一定是在条件队列中
      return false;
  if (node.next != null) // node.next不是null,一定是在同步队列中,反之则不一定,因为cpu分片执行,在某个瞬间会存在没有后继的情况
      return true;
  return findNodeFromTail(node); // 从尾部遍历一遍
 }
 private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
 }

执行到以上代码,说明当前线程已经成功入队,并被park了,如果从park中返回,会检查中断,上面已经分析过了,然后到acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; // 记录是否发生错误
    try {
        boolean interrupted = false; // 是否执行了中断
        for (;;) {
            final Node p = node.predecessor(); // 获取前驱
            if (p == head && tryAcquire(arg)) { // 如果是head且尝试获取锁成功,tryAcquire 模板方法,有具体子类实现
                setHead(node); // 设置head,同一时刻只有一个线程获取锁成功,这里setHead 是线程安全的,且一定能成功
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node); // 取消获取
    }
}

当节点进入到同步队列,会尝试获取资源,同时设置 savedState 的值,这个值则是代表当初释放锁的时候释放了多少重入次数。
整个执行的流程:

public final void signal() {
    if (!isHeldExclusively()) // 判断当前线程是否是获取独占锁的线程
        throw new IllegalMonitorStateException();
    Node first = firstWaiter; // 从首节点开始唤醒
    if (first != null)
        doSignal(first);
}
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null) // 如果首节点没有后继节点
            lastWaiter = null; // 尾节点指向null
        first.nextWaiter = null; // 释放当前节点
    } while (!transferForSignal(first) && // 如果当前first节点已经取消且新的首节点不是null,则继续循环唤醒
             (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
      
      if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // cas失败,说明节点已经取消
          return false;

     
      Node p = enq(node); // 加入同步队列,并返回前驱节点
      int ws = p.waitStatus; // 前驱节点的状态
      /**
       * 如果前驱状态大于0(取消),或者设置前驱节点状态失败(设置前驱节点为 Node.SIGNAL的目的是告诉前驱节点,释放锁之前记得唤醒我),就唤醒当前节点,保证同步队列的健壮性
       */
      if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
          LockSupport.unpark(node.thread);
      return true;
  }

参考:segmentfault.com/a/119000003…