AbstractQueuedSynchronizer独占锁之await(),signal()原理

上一篇讲了ReentrantLock加锁和释放锁在AQS中的过程,这篇文章一起看看在有条件变量ConditionObject参与的情况下的ReentrantLock的逻辑流程。

上一篇连接:

AbstractQueuedSynchronizer之独占锁源码阅读

或者 https://blog.csdn.net/snow_0418/article/details/104475379

​ LeetCode上面有一道多线程的题,交替打印FooBar 题目要求2个线程交替分别打印"Foo",“Bar” n次。下面是我用ReentrantLock的一种解法,并不是最优(synchronized关键字 +this应该是比ReentrantLock的实现要更节省内存),仅仅是为了这里来方便展示:

// 为了方便表达,这里将运行foo()函数的线程称为“线程F”,运行bar()的线称为“线程B”
public class FooBar {
    private int n;
  	// 声明一个可重入锁
    private final Lock lock = new ReentrantLock();
  	// 因为只有2个线程, 因此,一个线程在执行的时候,另一个线程在条件变量上await()
    private final Condition condition = lock.newCondition();
  	// 共享变量,用来标识该哪个线程执行
    private boolean fooTurn = true;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {

        for (int i = 0; i < n; i++) {
            try{
                lock.lock();
              	// 如果现在轮到线程B运行,获取锁那么线程F在Condition上await()
                while (!fooTurn) {
                    condition.await();
                }
                // printFoo.run() outputs "foo". Do not change or remove this line.
                printFoo.run();
              	// 线程F打印完,轮到线程B打印。
                fooTurn = false;
              	// 唤醒Conditon上等待的线程B
                condition.signal();
            } finally {
                lock.unlock();
            }

        }
    }

    public void bar(Runnable printBar) throws InterruptedException {

        for (int i = 0; i < n; i++) {
            try{
                lock.lock();
              	// 如果现在轮到线程F运行,获取锁那么线程B在Condition上await()
                while (fooTurn)
                    condition.await();
                // printBar.run() outputs "bar". Do not change or remove this line.
                printBar.run();
              	// 线程B打印完,轮到线程F打印。
                fooTurn = true;
              	// 唤醒Condition上等待的线程F
                condition.signal();
            } finally {
                lock.unlock();
            }

        }
    }
}

这里借助上一篇当中AQS的结构来说明使用条件变量下的流程。***(本次示例中只有一个条件变量condition)***

AQS中await()和signal()工作示意图

下面来看await()方法的代码:

// 构建一个新的节点添加到等待队列(同步队列上的该线程的节点会在fullyRelease()方法中被踢出队列)
private Node addConditionWaiter() {
  // 当前的最后一个等待节点
  Node t = lastWaiter;
  // 如果最后一个节点已经被取消了。那么清除出去
  if (t != null && t.waitStatus != Node.CONDITION) {
    unlinkCancelledWaiters();
    t = lastWaiter;
  }
  // 构建新的等待节点,waitStatus = Node.CONDITION  也就是-1
  Node node = new Node(Thread.currentThread(), Node.CONDITION);
  // 将最后一个等待节点加入到等待队列中
  if (t == null)
    firstWaiter = node;
  else
    t.nextWaiter = node;
  lastWaiter = node;
  return node;
}

// 这个方法之所以叫fullyRelease,是因为它会将所有获取锁的计数state全部释放掉。
final int fullyRelease(Node node) {
  boolean failed = true;
  try {
    // 获取当前锁的全部计数
    int savedState = getState();
    // release()方法释放锁,调用了子类的tryRelease()方法
    if (release(savedState)) {
      failed = false;
      return savedState;
    } else {
      // 
      throw new IllegalMonitorStateException();
    }
  } finally {
    // 如果释放锁失败则取消当前等待节点。
    if (failed)
      node.waitStatus = Node.CANCELLED;
  }
}

final boolean isOnSyncQueue(Node node) {
  // waitStatus == Node.CONDITION的话,说明当前节点是等待队列上的节点。fullyRelease()方法执行成功,释放锁成功。
  // 如果当前节点是同步队列上的节点,那么prev = null的话说明这个节点是头结点(thread == prev == null, next != null),或者是作为曾经的头结点已经被踢出同步队列了(thread == prev == next == null)
  if (node.waitStatus == Node.CONDITION || node.prev == null)
    return false;
  // 如果当前节点的next != null 那么当前节点一定还在同步队列上。
  // 因为 1.等待队列中的节点 prev == next == null 2. 已经被踢出同步队列的节点next也是null(见上面临近的这个if注释)
  if (node.next != null) // If has successor, it must be on queue
    return true;
  /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
  // 这个是小概率事件的检测,从尾部循环直到队列的尽头,查找当前节点是否在同步队列中。
  return findNodeFromTail(node);
}

public final void await() throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  // 在等待队列上添加一个新的节点。
  Node node = addConditionWaiter();
  // 需要注意的是,只有持有锁的线程,才能释放锁(否则会抛异常)。也就是说await()方法被调用时,当前线程还是持有锁的。
  // 完全释放当前线程持有的锁,释放后state = 0. 
  int savedState = fullyRelease(node);
  int interruptMode = 0;
  // isOnSyncQueue()方法判断当前线程是否还在同步队列上。不是,返回false。
  // !isOnSyncQueue()就为true, 那么当前线程成功的将自己添加到了等待队列,并且释放了锁。
  while (!isOnSyncQueue(node)) {
    // 释放锁和添加到等待队列成功之后,线程在这个调用上休眠并且被禁止调度,直到下次被唤醒。
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
      break;
  }
  // 被唤醒后,尝试重新获取锁,获取锁成功,获取锁失败的话会将前一个节点的waitStatus设置为Node.SIGNAL,以便适当的时候唤醒自己。
  if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
  if (node.nextWaiter != null) // clean up if cancelled
    unlinkCancelledWaiters();
  if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);
}


总的来说,await()方法在被唤醒前做的事情有下面几步:

  1. 用当前线程构建一个新的节点,加入到等待队列中;

  2. 完全释放当前线程持有的锁(也就是最终AQS中state = 0);

  3. 将当前线程陷入休眠并且禁止调度LockSupport.park();

  4. 等待被唤醒后再次获取锁。

    一定要注意的是只有持有锁的线程才能调用await()方法加入等待队列中,否则会抛出异常IllegalMonitorStateException()

而且看完await()方法之后,我有个疑问,如果当前线程是通过同步队列的head节点获取到锁的,那么当前线程在同步队列中的节点怎样了呢?(其实这个在上一篇的acquireQueued()方法中已经有答案了) 下面我们就来看一下条件变量和await()方法对应的signal()方法(还有signalAll()方法)。

下面来看看signal()代码:

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;
     // 将当前头结点踢出等待队列
     first.nextWaiter = null;
   } while (
     // 如果当前节点唤醒不成功而且下一个节点不为空,那么就依次唤醒,直到唤醒一个节点。
     // 如果唤醒当前节点成功的话,会将当前节点加入到同步队列中,循环终止。
     !transferForSignal(first) && (first = firstWaiter) != null
   );
 }

// 唤醒等待队列中的当前节点
final boolean transferForSignal(Node node) {
  // 尝试更新当前节点的状态为0,不成功也没关系,harmlessly wrong.
  if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
    return false;

	// 将当前节点(已经被从等待队列当中踢出来了)加入到同步队列。返回的是当前节点在同步队列中的上一个节点。这里enq()方法并没有将前一个节点的waitStatus设置为SIGNAL来唤醒自己,因为当前节点在acquireQueued()方法中采取无限循环(自旋)的方式判断它自己的前一个节点是否为head节点,并且尝试获取锁,获取锁才会return。
  Node p = enq(node);
  int ws = p.waitStatus;
  // 如果上一个接单已经被取消或者更新上一个节点的waitStatus为SIGNAL失败,那么直接唤醒当前节点中的线程重新加入同步队列(被唤醒的线程会从await()方法中的LockSupport.part()方法中醒来,继续获取锁)。
  if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
    LockSupport.unpark(node.thread);
  return true;
}

signal()方法的逻辑流程总结起来就是2步:1. 更新等待队列 2. 唤醒等待队列中第一个可以被唤醒的节点

同样的signalAll()方法只是把第二步中的逻辑改为了唤醒等待队列中的所有等待节点,并且加入到同步队列中,然后尝试获取锁。相信有了对signal()的理解,signalAll()理解起来并不难。

整理不易,最后希望大家关注我的公众号 : 青衣慕雪,一起探讨学习
青衣慕雪

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值