之前给大家仅仅介绍了下
ReentrantLock中相关的API流程,接下来就讲讲里面的Condition。在之前介绍JAVA并发编程中有提及到,它与重入锁的关系就相当于Object里面的等待和通知方法一样,只不过比Object实现线程间协作更加安全和高效。
Condition等待
在实现线程里面等待的时候,我们都知道调用condition.await()方法,使得当前线程进入等待状态。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();//将当前线程包装为Node类型存入到Condition里面的链表中
int savedState = fullyRelease(node);//释放当前线程占有的锁
int interruptMode = 0;
while (!isOnSyncQueue(node)) {//释放完毕后,遍历AQS的队列,看当前节点是否在队列中,不在 说明它还没有竞争锁的资格,所以继续将自己沉睡。直到它被加入到队列中。
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);
}
上面就能看到方法内部的执行流程。我们就详细的进去看看。首先进入的是封装当前线程节点的方法:
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);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
上面函数的作用就是将当前的线程包装为一个Node节点存储在Condition接口的实现类ConditionObject里面,这里面维护了一个等待的队列。然后我们继续往下看:
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;
}
}
上面的函数就是将当前线程占有的锁进行释放,同时将之前占用锁的次数返回,用于之后的操作。继续往下走====>
final boolean isOnSyncQueue(Node node) {
//如果当前节点状态是CONDITION或node.prev是null,则证明当前节点在等待队列上而不是同步队列上。因为一个节点如果要加入同步队列,在加入前就会设置好prev字段。
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//如果node.next不为null,则一定在同步队列上,因为node.next是在节点加入同步队列后设置的
if (node.next != null)
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;
}
}
这上面的while循环就帮助我们确定了当前的节点肯定是在同步队列中,否则是不会进行到下面的步骤中,我们继续往下:
//当前的节点目前已经在同步队列上,但是不能保证在队首,所以这里将阻塞直到当前节点变成队首,同时让当前进程获得锁。然后继续执行当前等待的剩下代码。
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
剩下的就是取消链接的节点以及等待退出时候重新中断。
Condition唤醒
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);
}
上面的transferForSignal方法尝试唤醒当前节点,如果唤醒失败,则继续尝试唤醒当前节点的后继节点。
final boolean transferForSignal(Node node) {
//如果当前节点状态为CONDITION,则将状态改为0准备加入同步队列;如果当前状态不为CONDITION,说明该节点等待已被中断,则该方法返回false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将节点加入同步队列,返回的p是节点在同步队列中的先驱节点
Node p = enq(node);
int ws = p.waitStatus;
//如果先驱节点状态是大于0或者设置先驱节点的状态为SIGNAL失败。那么立即唤醒当前节点对应的线程。如果当前设置前驱节点状态为SIGNAL成功,那么就不需要马上唤醒线程了,当它的前驱节点成为同步队列的首节点且释放同步状态后,会自动唤醒它。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
Condition说明
当持有锁的线程开始进行await()调用:
- 构造一个等待节点加入等待队列队尾;
- 对应线程释放锁,以及从同步队列队首移除该节点
- 自旋等待,直到等待队列上面的节点移动到了同步队列或者被中断。
- 阻塞当前及诶单,直到它获得锁,也就是此时该节点排队排到了同步队列的队首。
当持有锁的线程开始进行signal()调用:
-
从等待队列队首开始,尝试对节点唤醒操作;如果节点是取消状态,则唤醒下一个节点。
-
唤醒该节点,首先将节点加入同步队列中,此时
await()中的步骤三的解锁条件开始。这里会有一个简单的判断:-
如果先驱节点的状态为取消或者设置先驱节点的状态为SIGNAL失败,则立即唤醒当前线程。此时
await()会完成步骤3,进入步骤4阶段。 -
如果状态设置成功为SIGNAL,那么就不立即唤醒,等到先驱节点变成同步队列的队首并释放了同步状态后,会自动唤醒当前节点对应的线程。这个时候步骤3才执行完成。
-
这个上面就讲解了Condition里面的等待以及唤醒操作。我们主要理解里面有等待队列以及同步队列相互之间的合作。如果从同步变成等待以及等待变成同步的操作。