JUC解析-AQS(2)

460 阅读5分钟

在上一篇AQS的文章中,主要介绍了资源同步的原理以及设计原则,在这里我们考虑另外一个比较重要的概念Condition。

解决的问题

Condition可以用来解决不同线程之间的消息同步问题

例如我们比较熟悉的生产者消费者问题就可以利用信号的机制来解决,当消费者所需资源不足的时候,可以等待在信号中,当生产者产生了新的资源,即可以通过信号唤醒消费者,下面我们详细的看下具体的实现原理。

实现原理

  • 每一个信号都存在一个队列,队列中存有等待此信号的所有等待线程,这个队列不同于AQS中的锁队列。
  • 当存在一个唤醒线程唤醒这个信号的时候,这个信号会将队列中的所有等待线程或者单个等待线程唤醒(具体依赖于唤醒的机制)。
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

信号等待

这里没有太多的逻辑,直接上源码吧

  public final void await() throws InterruptedException {
      //如果线程包含中断信号,则直接抛出异常
      if (Thread.interrupted())
          throw new InterruptedException();
      //将该线程插入该信号的等待队列中
      Node node = addConditionWaiter();
      //释放当前线程占有的锁资源,资源释放后,该线程会从AQS队列中剔除
      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;
      if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
      if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
        }

说明:

  • 多线程之间的消息同步都是涉及到资源的共享问题,因此在等待信号的时候必须要提前加锁,即等待线程加锁,然后执行await等待
  • 从上面的逻辑中可以看出,等待线程释放完锁后会从AQS队列中删除,因此该等待线程也会进入沉睡状态

信号唤醒

信号唤醒有两种方式:

  • 唤醒等待线程中的其中一个
  • 唤醒所有等待的线程
  //唤醒一个等待的有效线程线程,直到成功
  private void doSignal(Node first) {
    do {
      if ((firstWaiter = first.nextWaiter) == null)
        lastWaiter = null;
      first.nextWaiter = null;
    } while (!transferForSignal(first) &&
              (first = firstWaiter) != null);
  }
  
  //唤醒所有等待的有效线程
  private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
  }
  
  //等待线程的唤醒
  final boolean transferForSignal(Node node) {
    //设置等待线程的状态
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
      return false;
    //将线程重新加入AQS队列中
    Node p = enq(node);
    int ws = p.waitStatus;
    //如果线程取消(ws>0表示cancel)或者错误,才真正唤醒,正常逻辑不会执行
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
      LockSupport.unpark(node.thread);
    return true;
  }

说明:

  • 唤醒操作是另一个线程执行的,暂且叫唤醒线程,在执行唤醒操作之前也必须加锁。
  • transferForSignal函数中可以看出,信号唤醒并不是真正的唤醒等待线程,而只是将等待线程重新放到AQS队列中,到此为什么还没发生什么神奇的事情,接下来就要注意了:
  • 唤醒线程调用signal(假装)唤醒等待线程后,接着会释放锁占用的锁,此时奇迹发生了,由于此时AQS队列中只有等待线程,因此等待线程会被唤醒(为什么会被唤醒?看上一节的内容),还没完,等待线程还有事情要做:
  • 等待线程被唤醒后,会接着执行await中的流程(还记得acquireQueued函数的作用吗?),他会接着在AQS中试图获取锁,如果获取成功则会继续执行,失败,则会继续等待下一次唤醒.到此为止信号的等待和唤醒就介绍完了。

虚假唤醒

在这里我们顺便说一下虚假唤醒的问题,在上面的信号唤醒部分,我们最后说到当等待线程被唤醒的时候会接着执行await中的流程,其中一步就是acquireQueued,该函数上一节介绍过就是获取AQS锁,在这里可能会获取失败,此时这个等待线程可能会再次陷入等待,这就是虚假唤醒,现在原因知道了,怎么解决也好办了吧!

总结

最最后为了大家好理解,我们还是再总结下流程吧,假设存在两个线程1和2:

  1. 线程1调用lock成功,因此对应的节点被加入到AQS的队列中。
  2. 线程1调用await方法时,释放锁,对应的节点从AQS队列中移除。
  3. 线程1加入Condition等待队列中,等待signal信号。
  4. 线程1释放了锁,因此线程2被唤醒,并获取锁成功,加入到AQS队列中。
  5. 线程2调用signal方法,此时Condition的等待队列中只有线程1,因此被加入到AQS的等待队列中,注意,这个时候,线程1并没有被唤醒。
  6. 线程2的signal方法执行完毕,接着调用unLock释放锁。此时因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程1,线程1恢复执行。