上一篇讲了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)***
下面来看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()方法在被唤醒前做的事情有下面几步:
-
用当前线程构建一个新的节点,加入到等待队列中;
-
完全释放当前线程持有的锁(也就是最终AQS中state = 0);
-
将当前线程陷入休眠并且禁止调度LockSupport.park();
-
等待被唤醒后再次获取锁。
一定要注意的是只有持有锁的线程才能调用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()理解起来并不难。
整理不易,最后希望大家关注我的公众号 : 青衣慕雪,一起探讨学习