AbstractQueuedSynchronizer
LockSupport
unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。
“许可”是不能叠加的:当对一个线程执行park操作时,本次调用前的所有对该线程的unpark操作只存在一次“许可”。例如线程B连续三次被执行unpark操作,此时B没有被任何park操作阻塞,之后线程B被执行park操作时就会使用并清除这个许可,如果线程B再次被执行park操作,就进入等待状态。
对一个线程unpark操作可以发生在park操作前:如果park时发现已经存在“许可”,则不需要阻塞可直接接续执行。
park操作会让当前线程进入WAITING状态。在此状态下,有两种途径可以唤醒该线程:1.unpark();2.interrupt()。
一、简介
抽象队列同步器(AQS)是CLH队列的变种实现,AQS的每个节点代表一个锁的请求,每个节点记录当前请求锁的线程、前驱节点和后继节点等信息,支持共享锁模式和独占锁模式
Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
二、独占模式
1、加锁逻辑
acquire
方法用于独占锁的申请:
该方法首先会调用tryAcquire(arg)
尝试获取锁,如果获取成功则返回加锁成功,否则当前线程请求进入等待队列等待被持有锁的线程唤醒。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
第一步:调用tryAcquire(arg)
,如果获取成功则返回加锁成功。该方法在AQS中被调用会抛出异常,必须由子类将其重写,之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
第二步:当调用tryAcquire(arg)
加锁失败后,当前线程的申请需要进入等待队列等待持有锁的线程释放锁,即调用addWaiter(Node.EXCLUSIVE)
。当新节点入队列时,如果发现队列为空,就会初始化一个虚拟节点作为当前持有锁的节点,其前驱结点和线程信息为空,此时头节点和尾节点都指向该虚拟节点;然后再将当前节点入队列,此时队列中存在两个节点。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//当头节点为空时需要初始化
if (t == null) { // Must initialize
//cas将头节点设置成一个新的Node对象,其属性都为默认值
if (compareAndSetHead(new Node()))
//cas设置成功,再将尾节点也设置为新的节点
tail = head;
} else {
node.prev = t;
//节点入队列
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
第三步:当节点入队以后如果直接将其阻塞,为了尽可能减少阻塞的线程,AQS需要确认是否有必要将当前线程park
,如果不需要则当前线程会一直尝试获取锁,这一步在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
中实现。
该方法正常的退出方式是return语句,但是由于tryAcquire
方法由开发者自己实现,因此有可能会出现一些异常场景,例如方法抛出异常。因此acquireQueued
方法在自旋外加上了try-catch语句块保证当方法执行出错时会将当前节点取消并从等待移除。
//节点入队列以后如果自身是第一个排队的节点,则再次尝试获取锁,如果获取锁失败或者其并非是第一个排队的节点,检测是否需要park
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;
}
//检测是否需要park
if (shouldParkAfterFailedAcquire(p, node) &&
//如果需要park则在该方法中park
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在该方法中首先会获取等待队列中该节点的前驱节点,如果前驱节点为头节点,则表明当前节点是第一个排队的节点,尝试申请一次加锁,如果加锁成功则将自身设置为头节点(出队列),否则需要进入shouldParkAfterFailedAcquire(p, node)
判断是否需要阻塞线程。
加锁失败以后是否需要阻塞当前线程取决于shouldParkAfterFailedAcquire(p, node)返回的值:
- true:则线程调用后续的park逻辑并在被唤醒的时候检查当前线程是否已经被中断
- false:线程不需要park,继续自旋尝试获取锁
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
//如果前驱节点被取消,查找最接近的一个为被取消的节点,将当前节点作为其后继节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
由于在等待队列中当前节点需要由前驱节点唤醒,因此当前节点的状态维护在前驱节点的waitStatus中
waitStatus在节点初始化的时候并没有指定值,因此为默认值0,因此acquireQueued(final Node node, int arg)
方法至少需要两次调用shouldParkAfterFailedAcquire(Node pred, Node node)
方法:
- 第一次由于waitStatus=0, 使用CAS操作将其设置为
Node.SIGNAL
,返回false - 如果第一次设置成功,则第二次调用返回true,当前线程阻塞
节点进入等待队列中节点的可能情况:
-
2个节点:当新节点入队列,如果发现队列为空,就会初始化一个虚拟节点作为当前持有锁的节点,其前驱结点和线程信息为空,此时头节点和尾节点都指向该虚拟节点;然后再将当前节点入队列,此时队列中存在两个节点。
-
n个节点(n>2)
其他节点继续入队。
-
1个节点:假设当前队列中有一个节点在等待锁,即队列中存在两个节点,此时等待资源的节点加锁成功,就会将自己置为新的对头,并移除前驱结点,此时队列中只存在一个节点,并且此时会调用setHead(node)方法将节点的线程信息置空表示头节点已经持有锁。
2、解锁逻辑
release
方法首先会调用tryRelease
方法释放锁,tryRelease
逻辑同样必须有用户自己实现
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
如果tryRelease
方法解锁成功,则调用unparkSuccessor
方法唤醒等待队列中的节点获取锁。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor
会从尾节点开始查找指定node
第一个后继待唤醒节点。这是由于node.next有可能为null:在新增节点时,会首先将新节点的prev指针指向队列tail节点,然后使用CAS操作新节点入队,入队成功后新节点成为尾节点并且将原来的尾节点next指针指向新节点,在这个过程中如果节点入队成功但是还没来得及将原来的尾节点next指针指向新节点,就会出现node.next为null的情况,采用前驱节点查找可以避免这个问题。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
//将新节点的prev指针指向队列tail节点
node.prev = pred;
//CAS操作入队
if (compareAndSetTail(pred, node)) {
//新节点成为尾节点并且将原来的尾节点next指针指向新节点
pred.next = node;
return node;
}
}
enq(node);
return node;
}
三、共享模式
相较于独占模式的加锁解锁逻辑,AQS共享模式下并没有加锁和解锁的概念,只有资源的获取和释放,即使用该锁的所有线程共享一部分资源,当资源充足时可直接获取使用,当资源不足时需要等待其他线程释放资源,因此当线程释放资源时会唤醒等待资源的线程。
1、获取资源
tryAcquireShared
类似于tryAcquire
方法,同样需要由子类重写实现逻辑。tryAcquireShared
有两种返回值分别表示不同的涵义:
- 大于0或者等于0:表示当前线程获取资源成功,返回值表示剩余的资源数
- 小于0:表示当前线程获取资源失败
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
当获取资源失败时,当前线程就需要等待其他线程释放资源,因此需要自旋或者进入等待队列等待
doAcquireShared
类似于独占模式下的acquireQueued
方法,不同之处在于当线程获取资源成功以后,可能还会有剩余资源可以满足其他线程的需求,因此还需要唤醒后继节点。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
//如果资源满足当前线程需求,则分配完成后继续唤醒后继节点继续获取资源,否则需要park或者自旋等待资源
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate
相较于setHead
方法多了一个唤醒后继节点获取资源的步骤
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
2、释放资源
releaseShared
释放资源,如果释放成功会调用doReleaseShared
唤醒后继节点获取资源
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
doReleaseShared
方法会获取AQS队列头节点并检查其状态来决定对后继节点的操作,如果状态为SIGNAL,则说明后继节点等待被唤醒,因此尝试通过CAS操作将状态设置为0,如果设置成功则唤醒后继节点。关于这一步的CAS操作,其实在unparkSuccessor
方法中也会判断前驱节点的状态,同样也会在状态小于0时将其设置为0。
为何这里要做这一步,结合注释来看,我的理解是:doReleaseShared
运行在共享模式下,因此该方法有可能在某一时刻有多个线程进入,如果不加CAS操作检验状态是否已经被修改,unparkSuccessor
调用过程中或者调用前,head节点已经被变更,例如头节点已经被另一个线程唤醒并且获取到资源。为了避免这种情况,在调用unparkSuccessor
前先将head节点的状态置为0,此时其他节点修改就会失败而自旋。PROPAGATE其实是为了不影响线程其他地方对该结点的操作,而又为了标识doReleaseShared
在该方法中已经被修改的状态,当节点的状态被修改为PROPAGATE后,线程就不会执行任何操作,只检测头节点是否发生变更,如果未发生变更就退出自旋。在该方法中有可能会发生不必要的唤醒或者多余的唤醒,因为该方法检测到头节点发生变更会继续唤醒新的后继节点,但是在setHeadAndPropagate
方法中也调用了该方法,也就是被唤醒的线程同样会尝试唤醒后继节点,因此会发生一个现象:所有被唤醒的线程都在唤醒后继节点。因此一个节点可能已经被唤醒然后再次被唤醒,或者因为资源不足而阻塞,当再次被唤醒后还有可能会因为资源不足而阻塞。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
关于上述现象的一次复现,借助Semaphore
类来实现。
第一步、在如图所示位置打上断点,并将挂起方式设置为Thread
,所有的唤醒操作都将在断点处挂起。
第二步、创建Semaphore
对象并直接申请所有的资源,资源总数100。
第三部、创建100个获取资源的线程,因为第一步完成后已经没有资源可获取,因此这100个线程都将阻塞。
第四步、释放所有的资源,交个上述这100个线程去竞争。
第五步、观察断点处的线程和线程栈信息,相当多的线程处于RUNNING状态,这也应证了上述的想法:被唤醒的线程同样会尝试唤醒后继节点。
四、ConditionObject
ConditionObject
是AQS的内部类,实现了Condition
接口,提供条件锁的同步实现和可中断和非中断方式下等待、唤醒的方法。ConditionObject
是为并发编程中的同步提供了等待通知的实现方式,可以在不满足某个条件的时候挂起线程等待。直到满足某个条件的时候在唤醒线程。
ConditionObject
内部维护了由一个单向链表实现的等待队列,单向链表复用了AQS的Node
节点,但是不设置prev指针。
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
等待队列是一个FIFO的队列,在队列的每个节点都包含了一个线程引用,该线程就是在Condition
对象上等待的线程。每个调用了await
方法的线程都会进入到等待队列中去。
一个同步器中可以有多个等待队列,他们等待的条件是不一样的。
调用await
方法会将当前线程添加到队列并尝试唤醒AQS队列中的节点,唤醒成功后当前线程被park,唤醒失败抛出IllegalMonitorStateException
。当一个线程被ConditionObject
的signal
方法唤醒时,会调用acquireQueued
方法重新获取锁或者挂起等待资源。
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);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//当前线程被signal方法唤醒或者被中断,重新获取锁或者挂起等待资源
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;
}
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
//唤醒AQS等待队列中的节点
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
acquireQueued
方法并不会将当前节点转移到AQS阻塞队列中,因此必须保证当acquireQueued
调用时,节点必须已经存在于AQS队列。因此在await
方法中有了如下代码片段:
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//前面说过park可以被打断和唤醒,为了保证后续操作正常执行,当park被打断时会将节点转移到AQS队列中,如果被唤醒则说明其他线程已经准备就绪,可以直接执行
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//检查节点是否已经存在于AQS队列中,如果存在返回true,如果不存在返回false
final boolean isOnSyncQueue(Node node) {
//如果node还是condition节点或者前驱节点为空,则表示不在AQS队列中
//因为condition节点只能存在于ConditionObject单独维护的FIFO队列中,而队列中存在前驱节点为空时只能是头节点,头节点是一个虚拟节点
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//不满足以上的if分支条件并且该节点的后继节点不为null,则说明该节点已经存在于AQS队列
if (node.next != null) // If has successor, it must be on queue
return true;
//如果以上条件都不满足,则需要遍历队列查找这个节点是否存在,查找时需要使用prev指针,具体原因在解锁逻辑部分已详细说明
return findNodeFromTail(node);
}
上面提到必须保证当acquireQueued
调用时,被唤醒节点必须已经存在于AQS队列。当park线程是被其他线程打断时是由await
方法保证唤醒节点存在于AQS队列中,但是如果程序正常执行时,这一点则是由调用signal
方法的线程保证的。
唤醒AQS节点的线程必须是当前持有锁的线程,也就是说调用await
方法之前必须获取到锁,否则抛出IllegalMonitorStateException
。
调用signal
方法会唤醒ConditionObject
队列中第一个等待的节点,并将该节点从等待队列中移除,然后将其添加到AQS队列中
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
//将节点转移到AQS节点中并从当前队列移除该节点,如果转移失败,则说明该节点已被取消,直接移除
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//如果节点被取消返回false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将节点添加到AQS队列中
Node p = enq(node);
//如果前驱节点的状态为取消则恢复线程继续执行取消操作
//如果CAS将前驱节点的状态设置为SIGNAL失败,则前驱节点状态发生变更,前驱节点可能已经被取消,则需要唤醒当前节点的线程让其执行读取更新前驱节点的状态
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
五、AQS应用
ReentrantLock
ReentrantLock
实现了tryAcquire
的公平锁和非公平锁
-
公平锁:获取锁时如果当前是无锁状态,如果AQS等待队列中存在阻塞的节点则加锁失败并随后进入等待队列。如果持有锁的是当前线程,则不需要再次获取锁,增加锁的状态标识即可。
final void lock() { acquire(1); } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
-
非公平锁:获取锁时如果当前是无锁状态直接尝试获取锁,不管等待队列是否已经有阻塞的节点。如果持有锁的是当前线程,则不需要再次获取锁,增加锁的状态标识即可。
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
CountDownLatch
每次调用countDown
方法都会将资源减1,当资源-1操作等于0后,说明所有的资源都恰好被消耗,该方法返回true。根据AQS共享锁的释放资源的逻辑,会唤醒后继等待的节点。
public void countDown() {
sync.releaseShared(1);
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
Semaphore
非公平模式:当remaining<0时说明资源不够分配,直接返回即可,如果remaining>0则资源足够分配,CAS设置剩余资源并返回剩余资源数。按照CAS实现逻辑,当tryAcquireShared方法返回值小于0时,线程需要等待其他线程释放资源。
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
公平模式:相较于非公平模式下的资源获取,公平模式下资源获取首先需要检查AQS等待队列是否有节点正在等待资源,如果有则返回-1让当前线程排队等待资源。
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}