Java版本:8u261。
1 简介
AQS全称AbstractQueuedSynchronizer,是一个能多线程访问共享资源的同步器框架。作为Doug Lea大神设计出来的又一款优秀的并发框架,AQS的出现使得Java中终于可以有一个通用的并发处理机制。并且可以通过继承它,实现其中的方法,以此来实现想要的独占模式或共享模式,抑或是阻塞队列也可以通过AQS来很简单地实现出来。
一些常用的并发工具类的底层都是通过继承AQS来实现的,比如:ReentrantLock、Semaphore、CountDownLatch、ArrayBlockingQueue等(这些工具类也都是Doug Lea写的)。AQS中有几个重要的模块:
- state:用来记录可重入锁的上锁次数;
- exclusiveOwnerThread:AQS继承了AbstractOwnableSynchronizer,而其中有个属性exclusiveOwnerThread,用来记录当前独占锁的线程是谁;
- CLH同步队列:FIFO双向链表队列,此CLH队列是原CLH的变种,由原来的不断自旋改为了阻塞机制。队列中有头节点和尾节点两个指针,尾节点就是指向最后一个节点,而头节点为了便于判断,永远指向一个空节点,之后才是第一个有数据的节点;
- 条件队列:能够使某些线程一起等待某个条件具备时,才会被唤醒,唤醒后会被放到CLH队列中重新争夺锁资源。
AQS定义资源的访问方式有两种:
- 独占模式:只有一个线程能够获取锁,如ReentrantLock;
- 共享模式:多个线程可以同时获取到锁,如Semaphore、CountDownLatch和CyclicBarrier。
而上面所说的CLH队列和条件队列的节点都是AQS的一个内部类Node构造的,其中定义了一些节点的属性:
static final class Node {
/**
* 标记节点为共享模式
*/
static final Node SHARED = new Node();
/**
* 标记节点为独占模式
*/
static final Node EXCLUSIVE = null;
/**
* 标记节点是取消状态,CLH队列中等待超时或者被中断的线程,需要从CLH队列中去掉
*/
static final int CANCELLED = 1;
/**
* 该状态比较特殊,如果该节点的下一个节点是阻塞状态,则该节点处于SIGNAL状态
* 所以该状态表示的是下一个节点是否是阻塞状态,而不是表示的本节点的状态
*/
static final int SIGNAL = -1;
/**
* 该状态的节点会被放在条件队列中
*/
static final int CONDITION = -2;
/**
* 用在共享模式中,表示节点是可以唤醒传播的。CLH队列此时不需要等待前一个节点释放锁之后,该节点再获取锁
* 共享模式下所有处于该状态的节点都可以获取到锁,而这个传播唤醒的动作就是通过标记为PROPAGATE状态来实现
*/
static final int PROPAGATE = -3;
/**
* 记录当前节点的状态,除了上述四种状态外,还有一个初始状态0
*/
volatile int waitStatus;
/**
* CLH队列中用来表示前一个节点
*/
volatile Node prev;
/**
* CLH队列中用来表示后一个节点
*/
volatile Node next;
/**
* 用来记录当前被阻塞的线程
*/
volatile Thread thread;
/**
* 条件队列中用来表示下一个节点
*/
Node nextWaiter;
//...
}
AQS中使用到了模板方法模式,提供了一些方法供子类来实现,子类只需要实现这些方法即可,至于具体的队列的维护就不需要关心了,AQS中已经实现好了。
2 CLH队列
这里需要注意的一点是,head指针永远会指向一个空节点。如果当前节点被剔除掉,而后面的节点变成第一个节点的时候,此时就会清空该节点里面的内容(waitStatus不会被清除),将head指针指向它。这样做的目的是为了方便进行判断。
2.1 独占模式(ReentrantLock)
独占模式就是只有一个线程能获取到锁资源,独占模式用ReentrantLock来举例,ReentrantLock内部使用sync来继承AQS,有公平锁和非公平锁两种:
public class ReentrantLock implements Lock, Serializable {
//...
/**
* 内部调用AQS
*/
private final Sync sync;
/**
* 继承AQS的同步基础类
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
//...
}
/**
* 非公平锁
*/
static final class NonfairSync extends Sync {
//...
}
/**
* 公平锁
*/
static final class FairSync extends Sync {
//...
}
/**
* 默认创建非公平锁对象
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 创建公平锁或者非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//...
}
2.1.1 非公平锁
ReentrantLock的非公平锁方式下的lock方法:
/**
* ReentrantLock:
*/
public void lock() {
sync.lock();
}
final void lock() {
/*
首先直接尝试CAS方式加锁,如果成功了,就将exclusiveOwnerThread设置为当前线程
这也就是非公平锁的含义,每一个线程在进行加锁的时候,会首先尝试加锁,如果成功了,
就不用放在CLH队列中进行排队阻塞了
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//否则失败的话就进CLH队列中进行阻塞
acquire(1);
}
/**
* AbstractQueuedSynchronizer:
*/
public final void acquire(int arg) {
//首先尝试获取资源,如果失败了的话就添加一个新的独占节点,插入到CLH队列尾部
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
/*
因为本方法不是响应中断的,所以如果当前线程中断后被唤醒,就在此处继续将中断标志位重新置为true
(selfInterrupt方法内部就一句话:“Thread.currentThread().interrupt();”),而不是会抛异常
(需要使用者在调用lock方法后首先通过isInterrupted方法去进行判断,是否应该执行接下来的业务代码)
*/
selfInterrupt();
}
/**
* ReentrantLock:
* 第26行代码处:
* 尝试获取资源
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//acquires = 1
final Thread current = Thread.currentThread();
int c = getState();
//如果当前没有加锁的话
if (c == 0) {
//尝试CAS方式去修改state为1
if (compareAndSetState(0, acquires)) {
//设置当前独占锁拥有者为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//当前state不为0,则判断当前线程是否是之前加上锁的线程
else if (current == getExclusiveOwnerThread()) {
//如果是的话,说明此时是可重入锁,将state+1
int nextc = c + acquires;
//如果+1之后为负数,说明此时数据溢出了,抛出Error
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* AbstractQueuedSynchronizer:
* 第27行代码处:
* 在CLH队列中添加一个新的独占尾节点
*/
private Node addWaiter(Node mode) {
//把当前线程构建为一个新的节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//判断当前尾节点是否为null?不为null说明此时队列中有节点
if (pred != null) {
//把当前节点用尾插的方式来插入
node.prev = pred;
//CAS的方式将尾节点指向当前节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果队列为空,将队列初始化后插入当前节点
enq(node);
return node;
}
private Node enq(final Node node) {
/*
高并发情景下会有很多的CAS失败操作,而下面的死循环确保节点一定要插进队列中。上面的代码和
enq方法中的代码是类似的,也就是说上面操作是为了做快速修改,如果失败了,在enq方法中做兜底
*/
for (; ; ) {
Node t = tail;
//如果尾节点为null,说明此时CLH队列为空,需要初始化队列
if (t == null) {
//创建一个空的Node节点,并将头节点CAS指向它
if (compareAndSetHead(new Node()))
//同时将尾节点也指向这个新的节点
tail = head;
} else {
//如果CLH队列此时不为空,则像之前一样用尾插的方式插入该节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* 第27行代码处:
* 注意:本方法是整个AQS的精髓所在,完成了头节点尝试获取锁资源和其他节点被阻塞的全部过程
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
//获取当前节点的前一个节点
final Node p = node.predecessor();
/*
如果前一个节点是头节点,才可以尝试获取资源,也就是实际上的CLH队列中的第一个节点
队列中只有第一个节点才有资格去尝试获取锁资源(FIFO),如果获取到了就不用被阻塞了
获取到了说明在此刻,之前的资源已经被释放了
*/
if (p == head && tryAcquire(arg)) {
/*
头指针指向当前节点,意味着该节点将变成一个空节点(头节点永远会指向一个空节点)
因为在上一行的tryAcquire方法已经成功的情况下,就可以释放CLH队列中的该节点了
*/
setHead(node);
//断开前一个节点的next指针,这样它就成为了一个孤立节点,等待被GC
p.next = null;
failed = false;
return interrupted;
}
/*
走到这里说明要么前一个节点不是head节点,要么是head节点但是尝试加锁失败。此时将队列中当前
节点之前的一些CANCELLED状态的节点剔除;前一个节点状态如果为SIGNAL时,就会阻塞当前线程
这里的parkAndCheckInterrupt阻塞操作是很有意义的。因为如果不阻塞的话,那么获取不到资源的
线程可能会在这个死循环里面一直运行,会一直占用CPU资源
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//只是记录一个标志位而已,不会抛出InterruptedException异常。也就是说不会响应中断
interrupted = true;
}
} finally {
if (failed)
//如果tryAcquire方法中state+1溢出了,就会取消当前线程获取锁资源的请求
cancelAcquire(node);
}
}
/**
* 第140行代码处:
* 将node节点置为新的head节点,同时将其中的thread和prev属性置空
* (注意:这里并不会清空waitStatus值)
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
/**
* 第152行代码处:
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前一个节点的状态是SIGNAL,意味着当前节点可以被安全地阻塞
return true;
if (ws > 0) {
/*
从该节点往前寻找一个不是CANCELLED状态的节点(也就是处于正常阻塞状态的节点),
遍历过程中如果遇到了CANCELLED节点,会被剔除出CLH队列等待GC
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
如果前一个节点的状态是初始状态0或者是传播状态PROPAGATE时,CAS去修改其状态为SIGNAL,
因为当前节点最后是要被阻塞的,所以前一个节点的状态必须改为SIGNAL
走到这里最后会返回false,因为外面还有一个死循环,如果最后还能跳到这个方法里面的话,
如果之前CAS修改成功的话就会直接走进第一个if条件里面,返回true。然后当前线程被阻塞
CAS失败的话会再次进入到该分支中做修改
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* 第153行代码处:
* 阻塞当前节点,后续该节点如果被unpark唤醒的时候,会从第215行代码处唤醒往下执行,返回false
* 可能线程在等待的时候会被中断唤醒,本方法就返回了true。这个时候该线程就会处于一种不正确的状态
* 返回回去后会在第155行代码处设置中断位为true,并最终返回到了第27行代码处。注意到下面的第228
* 行代码处使用的是Thread.interrupted方法,也就是在返回true之后会清空中断状态,所以需要在上面
* 的第33行代码处调用selfInterrupt方法里面的interrupt方法来将中断标志位重新置为true
*/
private final boolean parkAndCheckInterrupt() {
//当前线程会被阻塞到这行代码处,停止往下运行,等待unpark唤醒
LockSupport.park(this);
/*
通过上面的解释,可能会觉得下面的Thread.interrupted方法有点多余,需要清除中断标志位,最后
还会将中断标志位重新置为true。那么此时为什么不直接调用isInterrupted方法呢?不用清除中断标
志位就行了啊?其实这里使用Thread.interrupted方法是有原因的:LockSupport.park的实现会调用
native方法,通过查看底层的HotSpot源码中的park方法可知:如果在调用park方法时发现当前中断标
志位已经为true了,此时就会直接return退出本方法了(同时不会清除中断标志位),也就不会再进
行后续的挂起线程的操作了。也就是说,如果是中断唤醒,假如没有这里的Thread.interrupted方法
来清除中断标志位,那么可能下一次加锁失败还是会走进当前park方法,而此时的中断标志位仍然为
true。但是如上面所说,进入park方法中并不会被阻塞,也就是此时的park方法会失效,会不断在
acquireQueued方法中自旋,造成CPU飙高的现象出现。所以这里的Thread.interrupted方法清除中断
标志位是为了让后续调用的park方法能继续被成功阻塞住
*/
return Thread.interrupted();
}
/**
* 第160行代码处:
* 取消当前线程获取锁资源的请求,并完成一些其他的收尾工作
*/
private void cancelAcquire(Node node) {
//非空校验
if (node == null)
return;
//节点里面的线程清空
node.thread = null;
/*
从该节点往前寻找一个不是CANCELLED状态的节点(也就是处于正常阻塞状态的节点),
相当于在退出前再做次清理工作。遍历过程中如果遇到了CANCELLED节点,会被剔除出
CLH队列等待GC
这里的实现逻辑是和shouldParkAfterFailedAcquire方法中是类似的,但是有一点
不同的是:这里并没有pred.next = node,而是延迟到了后面的CAS操作中
*/
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
/*
如果上面遍历时有CANCELLED节点,predNext就指向pred节点的下一个CANCELLED节点
如果上面遍历时没有CANCELLED节点,predNext就指向自己
*/
Node predNext = pred.next;
/*
将状态改为CANCELLED,也就是在取消获取锁资源。这里不用CAS来改状态是可以的,
因为改的是CANCELLED状态,其他节点遇到CANCELLED节点是会跳过的
*/
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
//如果当前节点是最后一个节点的时候,就剔除当前节点,将tail指针指向前一个节点
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//走到这里说明当前节点不是最后一个节点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
/*
如果head指针指向的不是pred节点,并且前一个节点是SIGNAL状态(或者可以设置为SIGNAL状态),
并且前一个节点的thread没被清空的话,那么只需要将pred节点和当前节点的后面一个节点连接起来就行了
*/
Node next = node.next;
if (next != null && next.waitStatus <= 0)
/*
这里只是设置了pred节点的next指针,而没有设置next.prev = pred。但无妨,在后续的操作中,
如果能走到shouldParkAfterFailedAcquire方法中,会再去修正prev指针的
*/
compareAndSetNext(pred, predNext, next);
} else {
/*
而如果head指针指向的是pred节点(或者pred节点的thread是为null的),那么就去唤醒当前节点的
下一个可以被唤醒的节点,以保证即使是在发生异常的时候,CLH队列中的节点也可以一直被唤醒下去
当然,如果前一个节点本身就是SIGNAL状态,也是需要唤醒下一个节点的
*/
unparkSuccessor(node);
}
/*
node.next指向自己,断开该节点,同时要保证next指针一定要有值,
因为后续在条件队列的isOnSyncQueue方法中会判断节点是否在CLH队列中
其中有一条就是以判断node.next是否为null为准则,如果不为null,就说明
该节点还在CLH队列中
*/
node.next = node;
}
}
/**
* 第293行代码处:
* 唤醒下一个可以被唤醒的节点
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
/*
如果当前节点状态是SIGNAL或者PROPAGATE,将其CAS设置为初始状态0
因为后续会唤醒第一个被阻塞的节点,所以这里节点的状态如果还是SIGNAL就不正确了,
因为SIGNAL表示的是下一个节点是阻塞状态
*/
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//s是当前节点的下一个节点
Node s = node.next;
//如果下一个节点为null,或者状态为CANCELLED
if (s == null || s.waitStatus > 0) {
s = null;
/*
从CLH队列的尾节点向前遍历到该节点为止,找到该节点往后第一个处于正常阻塞状态的节点
至于为什么这里是从后往前遍历呢?原因就在于插入节点时是尾插法。保证了每个节点的prev
指针不为null,但是prev.next就不一定了
*/
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果找到了或者遍历之前的下一个节点本身就处于正常阻塞状态的话,就唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
ReentrantLock的unlock方法:
/**
* ReentrantLock:
*/
public void unlock() {
sync.release(1);
}
/**
* AbstractQueuedSynchronizer:
*/
public final boolean release(int arg) {
//释放一次锁,如果没有可重入锁的话,就进入到下面的if条件中
if (tryRelease(arg)) {
Node h = head;
/*
如果头节点存在且下一个节点处于阻塞状态的时候就唤醒下一个节点
因为在之前加锁方法中的shouldParkAfterFailedAcquire方法中,会将前一个节点的状态置为SIGNAL
所以这里判断waitStatus不为0就意味着下一个节点是阻塞状态,然后就可以唤醒了
如果为0就没有必要唤醒,因为下一个节点本身就是处于非阻塞状态
*/
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
/**
* ReentrantLock:
* 第13行代码处:
*/
protected final boolean tryRelease(int releases) {
//c = state - 1
int c = getState() - releases;
//如果当前线程不是上锁时的线程,则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果减完1后的state是0的话,也就是没有可重入锁发生的情况,则可以将独占锁拥有者设置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//设置state为减完1后的结果
setState(c);
return free;
}
2.1.2 公平锁
ReentrantLock的公平锁和非公平锁的实现上的区别寥寥无几,只有lock方法和tryAcquire方法是不同的(包括unlock解锁方法的实现都是一样的),也就是FairSync类中覆写的两个方法。所以下面就来看一下这两个方法的实现:
/**
* ReentrantLock:
*/
final void lock() {
/*
可以看到在公平锁模式下,只是调用了acquire方法而已。而在非公平锁模式下,会首先执行
compareAndSetState,如果CAS失败才调用acquire方法。这个意思也就是说:非公平锁
模式下的每个线程在加锁时会首先尝试加一下锁,去赌一下此时是否释放锁了。如果释放了,
那么此时的这个线程就能抢到锁,相当于插队了(这也就是“非公平”的含义)。如果没抢到就
继续去CLH队列中排队。而公平锁模式下的每个线程加锁时都只是会去乖乖排队而已
*/
acquire(1);
}
/**
* 可以看到公平锁模式下的tryAcquire方法和非公平锁模式下的nonfairTryAcquire方法的区别
* 仅仅是多调用了一次hasQueuedPredecessors方法,其他都是一样的。所以下面就来看一下该
* 方法的实现
*/
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;
}
/**
* 第24行代码处:
* 本方法是用来判断CLH队列中是否已经有不是当前线程的其他节点,
* 因为CLH队列都是FIFO的,head.next节点一定是等待时间最久的,
* 所以只需要跟它比较就行了。这里也就是在找CLH队列中是否有线程
* 的等待获取锁的时间比当前线程的还要长。如果有的话当前线程就
* 不会继续后面的加锁操作(这里再次体现“公平”的含义),没有
* 才会尝试加锁
*/
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/*
<1>首先判断head和tail是否不等,如果相等的话有两种情况:head和tail都为null,或者是head和tail
都指向那个空节点(当最后仅剩下两个节点的时候(一个空节点和一个真正等待的节点),此时再唤醒节点
的话,CLH队列中此时就会仅剩一个空节点了)。不管属于哪种,都代表着此时的CLH队列中没有在阻塞着的
节点了,那么这个时候当前线程就可以尝试加锁了;
<2.1>如果此时CLH队列中有节点的话,那么就判断一下head.next是否为空。我能想到的一种极端场景是:
假设此时CLH队列中仅有一个空节点(head和tail都指向它),就在此刻一个新的节点需要进入CLH队列里,
它走到了addWaiter方法中,在执行完了compareAndSetTail后,但是还没执行下面的“pred.next = node;”
之前,那么当前线程获取到的tail和head之间就仅有一个prev指针相连,而next指针此时还没有进行连接
那么此时获取到的head.next就是null了,这种情况下当前线程也不会尝试加锁,而是去CLH队列中排队
(这种情况下虽然h.next是null,但是是有一个等待时间比当前线程还久的节点的,只不过它的指针还没有
来得及连接上而已。所以当前节点会继续去排队,以此体现“公平”的含义);
<2.2>如果此时CLH队列中有节点,并且不属于上面第2.1条件中的特殊情况的话,还会去判断head.next
是否是当前线程。这个时候出现的场景就是:当前线程会在CLH队列中的head.next处,然后当前线程会再次在
本方法中进行判断。那么这是怎么发生的呢?一种可能的情况是:当之前持有锁的线程执行完毕释放了之后,
这个时候的队头节点会被唤醒,从而走到acquireQueued方法中的tryAcquire方法处,然后再走到本方法中
这个时候的当前线程就是被唤醒的这个线程,所以s.thread != Thread.currentThread()这个条件不成立,
此时当前线程就可以尝试加锁了。如果head.next不是当前线程,也就是当前线程不是等待时间最久的那个线程
此时就不会去加锁而是去排队去了(再次体现“公平”的含义)
*/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
2.2 共享模式(Semaphore)
共享模式就是有多个线程可以同时拿到锁资源,共享模式用Semaphore来举例,其与ReentrantLock的结构类似,也有公平和非公平两种模式:
public class Semaphore implements Serializable {
//...
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
//...
Sync(int permits) {
setState(permits);
}
//...
}
static final class NonfairSync extends Sync {
//...
NonfairSync(int permits) {
super(permits);
}
//...
}
static final class FairSync extends Sync {
//...
FairSync(int permits) {
super(permits);
}
//...
}
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//...
}
调用构造方法时需要传入一个控制同时并发次数的参数permits,该值会赋值给AQS的state(注意:这里是可以赋值成小于等于0的参数的,如果acquire的参数没有设置好的话,所有线程可能都会一直处于阻塞状态而无法被唤醒)。
2.2.1 非公平锁
Semaphore的非公平锁方式下的acquire方法:
/**
* Semaphore:
*/
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* AbstractQueuedSynchronizer:
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//arg = 1
//如果当前线程已经中断了,直接抛出异常。因为被中断了就没有意义再去获取锁资源了
if (Thread.interrupted())
throw new InterruptedException();
//尝试去获取共享资源
if (tryAcquireShared(arg) < 0)
//获取资源失败的话,进CLH队列进行排队等待
doAcquireSharedInterruptibly(arg);
}
/**
* Semaphore:
* 第18行代码处:
*/
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
//acquires = 1
for (; ; ) {
int available = getState();
int remaining = available - acquires;
/*
如果剩余资源小于0或者CAS设置state-1成功了的话,退出死循环
注意,这里不需要判断溢出了,因为这里是在做state-1
*/
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
/**
* AbstractQueuedSynchronizer:
* 第20行代码处:
* 和独占模式下的acquireQueued方法的代码类似,只不过这里是共享模式下的响应中断模式
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//CLH队列尾加入一个新的共享节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (; ; ) {
//获取当前节点的前一个节点
final Node p = node.predecessor();
if (p == head) {
/*
和独占模式一样,只有前一个节点是头节点,也就是当前节点
是实际上的第一个等待着的节点的时候才尝试获取资源(FIFO)
*/
int r = tryAcquireShared(arg);
if (r >= 0) {
/*
r大于等于0说明此时还有锁资源(等于0说明锁资源被当前线程拿走后就没了),
设置头节点,并且通知后面的节点也获取锁资源。独占锁和共享锁的差异点就在于此,
共享锁在前一个节点获取资源后,会通知后续的节点也一起来获取
*/
setHeadAndPropagate(node, r);
p.next = null;
failed = false;
return;
}
}
/*
和独占模式一样,将CLH队列中当前节点之前的一些CANCELLED状态的节点剔除;前一个节点状态如果
为SIGNAL时,就会阻塞当前线程。不同的是,这里会抛出异常,而不是独占模式的会设定中断位为true
即响应中断模式,如果线程被中断了会抛出InterruptedException
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
//如果线程被中断后唤醒,就会取消当前线程获取锁资源的请求
cancelAcquire(node);
}
}
/**
* 第72行代码处:
*/
private void setHeadAndPropagate(Node node, int propagate) {
//记录旧head节点
Node h = head;
//执行完setHead方法后,node节点成为新的head节点
setHead(node);
/*
<1>propagate>0表示还有剩余锁资源;
<2>旧head节点的状态<0(旧head节点是null这个条件是为了调用waitStatus时防止空指针异常);
<3>新head节点的状态<0(新head节点是null这个条件是为了调用waitStatus时防止空指针异常)
这些条件满足其一就会尝试调用doReleaseShared方法来唤醒后面的节点
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
/*
具体是否会调用doReleaseShared方法还需要判断node是最后一个节点或者node的下一个节点是
共享节点的时候才去唤醒(判断s是否为null一方面也是为了后面判断s是否是共享节点时不会抛
出空指针异常;但更重要的原因是因为如果node是CLH队列中的最后一个节点的话,这个时候虽然
拿到的s是null,但如果此时有其他的线程在CLH队列中新添加了一个节点后,此处并不能及时感
知到这个变化。于是此时也会走进doReleaseShared方法中去处理这种情况(当然,如果没有发生
多线程插入节点的时候,多调用一次doReleaseShared方法也是无妨的,在该方法里面会过滤掉这
种情况)。同时这里会特殊判断共享节点是因为CLH队列中可能会存在独占节点和共享节点共存的
场景出现,也就是ReentrantReadWriteLock读写锁的场景。这里会一直传播唤醒共享节点直到遇
到一个独占节点为止,后面的节点不管是独占或共享状态都不会再被唤醒了)
*/
if (s == null || s.isShared())
doReleaseShared();
}
}
/**
* 唤醒后续节点(加锁和释放锁都会调用本方法)
*/
private void doReleaseShared() {
for (; ; ) {
Node h = head;
//h != null && h != tail说明此时CLH队列中至少有两个节点(包括空节点),即至少含有一个真正在等待着的节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
/*
因为下面要唤醒下一个节点,所以将头节点的状态SIGNAL改为0(因为SIGNAL表示的是下一个节点是阻塞状态)
如果CAS没成功,就继续尝试
*/
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒下一个可以被唤醒的节点
unparkSuccessor(h);
} else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
/*
需要注意的是,在共享锁模式下,不论是acquire方法还是release方法,都会调用到doReleaseShared的,
而且每个方法也可能有多个线程在调用。也就是说doReleaseShared方法会有多个线程在调用。假如此时有
多个线程进入到第132行代码处,而其中一个线程先执行了第136行代码处的if条件,将头节点状态改为了0
而剩下的线程就不能跳进第136行代码处的if条件中,而只能走到第145行代码处,ws == 0条件满足,
于是剩下的线程就去CAS竞争修改头节点状态为PROPAGATE(表示需要将唤醒动作向后继续传播)。修改成功的
那个线程就跳到了第181行代码处,进行下个判断逻辑,而再剩下的那些线程就让它们继续循环就行了
(剩下的那些线程会发现head节点此时已经变成了PROPAGATE状态,于是会在下一次循环的第132行代码处
和第181行代码处两次判断head指针是否指向了同一个节点(包括之前那个CAS修改成功的线程和执行唤醒
动作的线程最后也会走到这里)。如果相同了,说明:
<1>可能是当前唤醒传播停止了(每个被唤醒的线程都可能会走入到本方法中的unparkSuccessor处
唤醒下一个节点,相当于把唤醒动作“传播”下去。同时每次唤醒后会变更head指针,如果head不发生变动了,
就说明唤醒传播停止了(注意上面所说的读写锁场景,也有可能是遇到了一个独占节点才停止的));
<2>可能是将要唤醒下一个节点但还没唤醒前的瞬间
不管是属于哪种情况,这些线程都可以退出了(第二种情况下只要等下一个节点唤醒并抢到锁后,还是会走到
本方法里面的,也就是会将唤醒动作继续传播下去。但那个时候就不需要这些线程来操心了,只需要保证唤醒
能一直传播下去就OK))
总结一下:因为head节点的状态为0就说明此时是一个中间过渡状态,最简单的情况下只有这个线程以及它所
唤醒的下个线程们在一直传递地唤醒着,是不会走入到145行代码处的if条件中来的。而如果有线程能走到这里,
就说明此时在doReleaseShared方法也就是本方法中有多个线程在同时调用着。PROPAGATE状态的出现,
我认为是为了创造出一种区别于SIGNAL状态的另外一种状态(因为SIGNAL状态的含义定义死了就是代表后一个
节点是阻塞状态,所以这里不能用SIGNAL状态来代替)。这个时候将head节点由原来的0置为PROPAGATE状态,
以此来保证之前的那些线程也可以读取到此时旧的head节点状态是PROPAGATE,是<0的,从而可以调用到
doReleaseShared方法继续去唤醒下一个节点,也就是将唤醒动作传播下去(在之前某个版本的
setHeadAndPropagate方法中,if条件中是没有最后那两个判断新head节点状态的条件的。如果是这样的话,
我上面的这些分析就是没问题的,但是后来不知道为什么又添加了那两个条件,这个时候的解释就略显苍白了
(因为即使没有PROPAGATE状态,这些获取锁的线程虽然拿到旧的head节点状态是0,但是此时获取到的新的head
节点也就是它们自己,其状态肯定是<0的,所以一样会走doReleaseShared方法)。但是之前确实是这样的,
也就是PROPAGATE状态添加的本意就是为了将唤醒传播下去,可能是后来为了修复某个bug,就又做了些改动
吧,这里就不再深究了)
*/
continue;
}
if (h == head)
break;
}
}
Semaphore的release方法:
/**
* Semaphore:
*/
public void release() {
sync.releaseShared(1);
}
/**
* AbstractQueuedSynchronizer:
*/
public final boolean releaseShared(int arg) {
//arg = 1
//释放锁资源,也就是做state+1的操作
if (tryReleaseShared(arg)) {
/*
唤醒后续可以被唤醒的节点
从这里就可以看出,在共享锁模式下,不仅释放锁的方法可以唤醒节点,加锁的方法也会触发唤醒后续节点的操作
*/
doReleaseShared();
return true;
}
return false;
}
/**
* Semaphore:
* 第14行代码处:
*/
protected final boolean tryReleaseShared(int releases) {
//releases = 1
for (; ; ) {
int current = getState();
int next = current + releases;
//如果超出int最大值,则抛出Error。同时如果传进来的releases本身就小于0的话,也会抛出Error
if (next < current)
throw new Error("Maximum permit count exceeded");
//CAS修改state+1
if (compareAndSetState(current, next))
return true;
}
}
值得一提的是:纵观整个AQS的源码,只有在doReleaseShared方法中具体用到了PROPAGATE这个状态,在其他地方都是没有显式用到的,那么可能就会对这个状态存在的意义有些许质疑了。其实在早期版本的AQS源码中是没有PROPAGATE这个状态的,之所以要引入它是为了解决一个bug(JDK-6801020):
从上面可以看到,这个bug是在Java 7中修复的(在Java 6中的一些版本中也已经添加了PROPAGATE状态),同时在bug清单的下面也贴出了可能出现bug的测试代码。那么下面就来看一下离现在非常久远的Java 5u22中的该处代码是如何实现的:
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
if (s == null || s.isShared())
unparkSuccessor(node);
}
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
可以看到,早期版本的实现相比于现在的实现来说简单了很多,总结起来最主要的区别有以下几个:
- 在setHeadAndPropagate方法中,早期版本对节点waitStatus状态的判断只是!=0,而现在改为了<0;
- 早期版本的releaseShared方法中的执行逻辑和独占锁下的release方法是一样的,而现在将具体的唤醒逻辑写在了doReleaseShared方法里面,和setHeadAndPropagate方法共同调用。
而可能出现bug的测试代码如下:
import java.util.concurrent.Semaphore;
public class TestSemaphore {
private static Semaphore sem = new Semaphore(0);
private static class Thread1 extends Thread {
@Override
public void run() {
sem.acquireUninterruptibly();
}
}
private static class Thread2 extends Thread {
@Override
public void run() {
sem.release();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
Thread t1 = new Thread1();
Thread t2 = new Thread1();
Thread t3 = new Thread2();
Thread t4 = new Thread2();
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(i);
}
}
}
其实上面所做的操作无非就是创建了四个线程:t1和t2用于获取信号量,而t3和t4用于释放信号量,其中的10000000次for循环是为了放大出现bug的几率,join操作是为了阻塞主线程。现在就可以说明一下出现bug的现象了:也就是这里可能会出现线程被hang住的情况发生(遗憾的是,我并没有模拟出来这个bug)。
可以想象这样一种场景:假如说当前CLH队列中有一个空节点和两个被阻塞的节点(t1和t2想要获取信号量但获取不到被阻塞在CLH队列中(state初始为0)):head->t1->t2(tail)。
- 时刻1:t3调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点不为null并且waitStatus为-1,于是继续调用unparkSuccessor方法,在该方法中会将head的waitStatus改为0;
- 时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时还没有调用接下来的setHeadAndPropagate方法;
- 时刻3:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点虽然不为null,但是waitStatus为0,所以就不会执行unparkSuccessor方法;
- 时刻4:t1执行setHeadAndPropagate->setHead,将头节点置为自己。但在此时propagate也就是剩余的state已经为0了(propagate是在时刻2时通过传参的方式传进来的,那个时候-1后剩余的state是0),所以也不会执行unparkSuccessor方法。
至此可以发现一轮循环走完后,CLH队列中的t2线程永远不会被唤醒,主线程也就永远处在阻塞中,这里也就出现了bug。那么来看一下现在的AQS代码在引入了PROPAGATE状态后,在面对同样的场景下是如何解决这个bug的:
- 时刻1:t3调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法;
- 时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时还没有调用接下来的setHeadAndPropagate方法;
- 时刻3:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,同时继续调用doReleaseShared方法,此时会将head的waitStatus改为PROPAGATE;
- 时刻4:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。虽然此时propagate依旧是0,但是“h.waitStatus < 0”这个条件是满足的(h现在是PROPAGATE状态),同时下一个节点也就是t2也是共享节点,所以会执行doReleaseShared方法,将新的head节点(t1)的waitStatus改为0,同时调用unparkSuccessor方法,此时也就会唤醒t2了。
至此就可以看出,在引入了PROPAGATE状态后,可以有效避免在高并发场景下可能出现的、线程没有被成功唤醒的情况出现。
2.2.2 公平锁
同ReentrantLock一样,Semaphore的公平锁和非公平锁实现上的区别也非常少,只有tryAcquireShared方法是不同的。所以下面就来看一下这个方法的实现:
/**
* Semaphore:
*/
protected int tryAcquireShared(int acquires) {
for (; ; ) {
/*
可以看到公平锁模式下的tryAcquireShared方法和非公平锁模式下的nonfairTryAcquireShared方法的区别
一样是多调用了一次hasQueuedPredecessors方法,以此来判断CLH队列中是否有线程的等待获取锁的时间
比当前线程的还要长。如果有的话就会直接返回-1,也就是获取资源失败,然后会进CLH队列进行排队等待
(体现“公平”的含义);没有的话就会去进行state-1,然后返回剩余的锁资源
*/
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
3 条件队列(ArrayBlockingQueue)
因为CLH队列中的线程,什么线程获取到锁,什么线程进入队列排队,什么线程释放锁,这些都是不受我们控制的。所以条件队列的出现为我们提供了主动式地、只有满足指定的条件后才能线程阻塞和唤醒的方式。对于条件队列首先需要说明一些概念:条件队列是AQS中除了CLH队列之外的另一种队列,每创建一个Condition实际上就是创建了一个条件队列,而每调用一次await方法实际上就是往条件队列中入队,每调用一次signal方法实际上就是往条件队列中出队。不像CLH队列上节点的状态有多个,条件队列上节点的状态只有一个:CONDITION。所以如果条件队列上一个节点不再是CONDITION状态时,就意味着这个节点该出队了。需要注意的是,条件队列只能运行在独占模式下。
一般在使用条件队列作为阻塞队列来使用时都会创建两个条件队列:notFull和notEmpty。notFull表示当条件队列已满的时候,put方法会处于等待状态,直到队列没满;notEmpty表示当条件队列为空的时候,take方法会处于等待状态,直到队列有数据了。
而notFull.signal方法和notEmpty.signal方法会将条件队列上的节点移到CLH队列中(每次只转移一个)。也就是说,存在一个节点从条件队列被转移到CLH队列的情况发生。同时也意味着,条件队列上不会发生锁资源竞争,所有的锁竞争都是发生在CLH队列上的。
其他一些条件队列和CLH队列之间的差异如下:
- 条件队列使用nextWaiter指针来指向下一个节点,是一个单向链表结构,不同于CLH队列的双向链表结构;
- 条件队列使用firstWaiter和lastWaiter来指向头尾指针,不同于CLH队列的head和tail;
- 条件队列中的第一个节点也不会像CLH队列一样,是一个特殊的空节点;
- 不同于CLH队列中会用很多的CAS操作来控制并发,条件队列进队列的前提是已经获取到了独占锁资源,所以很多地方不需要考虑并发。
下面就是具体的源码分析了。条件队列以ArrayBlockingQueue来举例:
3.1 构造器
/**
* ArrayBlockingQueue:
*/
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
//存放实际数据的数组
this.items = new Object[capacity];
//独占锁使用ReentrantLock来实现(fair表示的就是公平锁还是非公平锁,默认为非公平锁)
lock = new ReentrantLock(fair);
//notEmpty条件队列
notEmpty = lock.newCondition();
//notFull条件队列
notFull = lock.newCondition();
}
3.2 put方法
/**
* ArrayBlockingQueue:
*/
public void put(E e) throws InterruptedException {
//非空校验
checkNotNull(e);
final ReentrantLock lock = this.lock;
/*
获取独占锁资源,响应中断模式。其实现代码和lock方法还有Semaphore的acquire方法是类似的
因为这里分析的是条件队列,于是就不再分析该方法的细节了
*/
lock.lockInterruptibly();
try {
while (count == items.length)
//如果数组中数据已经满了的话,就在notFull中入队一个新节点,并阻塞当前线程
notFull.await();
//添加数组元素并唤醒notEmpty
enqueue(e);
} finally {
//释放锁资源
lock.unlock();
}
}
/**
* AbstractQueuedSynchronizer:
* 第16行代码处:
*/
public final void await() throws InterruptedException {
//如果当前线程被中断就抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//把当前节点加入到条件队列中
Node node = addConditionWaiter();
//释放之前获取到的锁资源,因为后续会阻塞该线程,所以如果不释放的话,其他线程将会等待该线程被唤醒
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果当前节点不在CLH队列中则阻塞住,等待unpark唤醒
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
/*
这里被唤醒可能是正常的signal操作也可能是被中断了。但无论是哪种情况,都会将当前节点插入到CLH队列尾,
并退出循环(注意,这里被唤醒除了上面两种情况之外,还有一种情况是操作系统级别的虚假唤醒(spurious wakeup),
也就是当前线程毫无理由就会被唤醒了,所以上面需要使用while来规避掉这种情况)
*/
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//走到这里说明当前节点已经插入到了CLH队列中(被signal所唤醒或者被中断)。然后在CLH队列中进行获取锁资源的操作
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
/*
<<<THROW_IE和REINTERRUPT的解释详见transferAfterCancelledWait方法>>>
之前分析过的如果acquireQueued方法返回true,说明当前线程被中断了
返回true意味着在acquireQueued方法中此时会再一次被中断(注意,这意味着有两个代码点判断线程是否被中断:
一个是在第46行代码处,另一个是在acquireQueued方法里面),如果之前没有被中断,则interruptMode=0,
而在acquireQueued方法里面线程被中断返回了,这个时候将interruptMode重新修正为REINTERRUPT即可
至于为什么不修正为THROW_IE是因为在这种情况下,第40行代码处已经通过调用signal方法正常唤醒了,
节点已经放进了CLH队列中。而此时的中断是在signal操作之后,在第50行代码处去抢锁资源的时候发生的
这个时候中断不中断已经无所谓了,所以就不需要抛出InterruptedException
*/
interruptMode = REINTERRUPT;
/*
走到这里说明当前节点已经获取到了锁资源(获取不到的话就会被再次阻塞在acquireQueued方法里)
如果interruptMode=REINTERRUPT的话,说明之前已经调用过signal方法了,也就是说该节点已经从条件队列中剔除掉了,
nextWaiter指针肯定为空,所以在这种情况下是不需要执行unlinkCancelledWaiters方法的;而如果
interruptMode=THROW_IE的话,说明之前还没有调用过signal方法来从条件队列中剔除该节点。这个时候就需要调用
unlinkCancelledWaiters方法来剔除这个节点了(在之前的transferAfterCancelledWait方法中已经把该节点的状态
改为了初始状态0),顺便把所有其他不是CONDITION状态的节点也一并剔除掉。注意:如果当前节点是条件队列中的
最后一个节点的话,并不会被清理。无妨,等到下次添加节点或调用signal方法的时候就会被清理了
*/
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//根据不同模式处理中断(正常模式不需要处理)
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
/**
* 第34行代码处:
*/
private Node addConditionWaiter() {
Node t = lastWaiter;
/*
如果最后一个节点不是CONDITION状态,就删除条件队列中所有不是CONDITION状态的节点
至于为什么只需要判断最后一个节点的状态就能知道整个队列中是否有不是CONDITION的节点,后面会说明
*/
if (t != null && t.waitStatus != Node.CONDITION) {
//删除所有不是CONDITION状态的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//创建一个类型为CONDITION的新节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
//t为null意味着此时条件队列中为空,直接将头指针指向这个新节点即可
firstWaiter = node;
else
//t不为null的话就说明此时条件队列中有节点,直接在尾处加入这个新节点
t.nextWaiter = node;
//尾指针指向这个新节点,添加节点完毕
lastWaiter = node;
/*
注意,这里不用像CLH队列中的enq方法一样,如果插入失败就会自旋直到插入成功为止
因为此时还没有释放独占锁
*/
return node;
}
/**
* 第73行和第90行代码处:
* 删除条件队列当中所有不是CONDITION状态的节点
*/
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
/*
在下面的每次循环中,trail指向的是从头到循环的节点为止,最后一个是CONDITION状态的节点
这样做是因为要剔除队列中间不是CONDITION的节点,就需要保留上一个是CONDITION节点的指针,
然后直接trail.nextWaiter = next就可以断开了
*/
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
} else
trail = t;
t = next;
}
}
/**
* 第36行代码处:
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
/*
释放锁资源。注意这里是释放所有的锁,包括可重入锁有多次加锁的话,会一次性全部释放。因为在上一行
代码savedState存的是所有的锁资源,而这里就是释放这些所有的资源,这也就是方法名中“fully”的含义
*/
if (release(savedState)) {
failed = false;
return savedState;
} else {
/*
释放失败就抛异常,也就是说没有释放干净,可能是在并发的情景下state被修改了的原因,
也可能是其他原因。注意如果在这里抛出异常了那么会走第169行代码
*/
throw new IllegalMonitorStateException();
}
} finally {
/*
如果释放锁失败,就把节点置为CANCELLED状态。比较精妙的一点是,在之前第88行代码处,
判断条件队列中是否有不是CONDITION的节点时,只需要判断最后一个节点的状态是否是CONDITION就行了
按常理来说,是需要遍历整个队列才能知道的。但是条件队列每次添加新节点都是插在尾处,而如果释放锁失败,
会将这个新添加的、在队列尾巴的新节点置为CANCELLED状态。而之前的CONDITION节点必然都是在队头
因为如果此时再有新的节点入队的话,会首先在第90行代码处将所有不是CONDITION的节点都剔除了
也就是说无论什么情况下,如果队列中有不是CONDITION的节点,那它一定在队尾,所以只需要判断它就可以了
*/
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
/**
* 第39行和第246行代码处:
* 判断节点是否在CLH队列中
*/
final boolean isOnSyncQueue(Node node) {
/*
如果当前节点的状态是CONDITION或者节点没有prev指针(prev指针只在CLH队列中的节点有,
尾插法保证prev指针一定有)的话,就返回false
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//如果当前节点有next指针(next指针只在CLH队列中的节点有,条件队列中的节点是nextWaiter)的话,就返回true
if (node.next != null)
return true;
//如果上面无法快速判断的话,就只能从CLH队列中进行遍历,一个一个地去进行判断了
return findNodeFromTail(node);
}
/**
* 遍历判断当前节点是否在CLH队列其中
*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (; ; ) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
/**
* 第46行代码处:
* 如果当前线程没有被中断过,则返回0
* 如果当前线程被中断时没有被signal过,则返回THROW_IE
* 如果当前线程被中断时已经signal过了,则返回REINTERRUPT
*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
/**
* 本方法是用来判断当前线程被中断时有没有发生过signal,以此来区分出THROW_IE和REINTERRUPT。判断的依据是:
* 如果发生过signal,则当前节点的状态已经不是CONDITION了,并且在CLH队列中也能找到该节点。详见transferForSignal方法
* <p>
* THROW_IE:表示在线程中断发生时还没有调用过signal方法,这个时候我们将这个节点放进CLH队列中去抢资源,
* 直到抢到锁资源后,再把这个节点从CLH队列和条件队列中都删除掉,最后再抛出InterruptedException
* <p>
* REINTERRUPT:表示在线程中断发生时已经调用过signal方法了,这个时候发不发生中断实际上已经没有意义了,
* 因为该节点此时已经被放进到了CLH队列中。而且在signal方法中已经将这个节点从条件队列中剔除掉了
* 此时我们将这个节点放进CLH队列中去抢资源,直到抢到锁资源后(抢到资源的同时就会将这个节点从CLH队列中删除),
* 再次中断当前线程即可,并不会抛出InterruptedException
*/
final boolean transferAfterCancelledWait(Node node) {
//判断一下当前的节点状态是否是CONDITION
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
/*
如果CAS成功了就表示当前节点是CONDITION状态,此时就意味着interruptMode为THROW_IE
然后会进行CLH队列入队,随后进行抢锁资源的操作
*/
enq(node);
return true;
}
/*
如果CAS失败了的话就意味着当前节点已经不是CONDITION状态了,说明此时已经调用过signal方法了,
但是因为之前已经释放锁资源了,signal方法中的transferForSignal方法将节点状态改为CONDITION
和将节点入CLH队列的这两个操作不是原子操作,所以可能存在并发的问题。也就是说可能会存在将节点状态改为CONDITION后,
但是还没入CLH队列这个时间点。下面的代码考虑的就是这种场景。这个时候只需要不断让渡当前线程资源,
等待signal方法将节点添加CLH队列完毕后即可
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
/**
* 第76行代码处:
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
//如果是THROW_IE最终就会抛出InterruptedException异常
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
//如果是REINTERRUPT就仅仅是“中断”当前线程而已(只是设置中断标志位为true)
selfInterrupt();
}
/**
* ArrayBlockingQueue:
* 第18行代码处:
*/
private void enqueue(E x) {
final Object[] items = this.items;
//插入数据
items[putIndex] = x;
//putIndex记录的是下次插入的位置。如果putIndex已经是最后一个了,重新复位为0,意味着数据可能会被覆盖
if (++putIndex == items.length)
putIndex = 0;
//当前数组中的数量+1
count++;
/*
如果notEmpty条件队列不为空的话,唤醒notEmpty条件队列中的第一个节点去CLH队列当中去排队抢资源
如果notEmpty里没有节点的话,说明此时数组没空。signal方法将不会有任何作用,因为此时没有阻塞住的take线程
*/
notEmpty.signal();
}
/**
* AbstractQueuedSynchronizer:
*/
public final void signal() {
//如果当前线程不是加锁时候的线程,就抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//如果notEmpty条件队列中有节点的话,就通知去CLH队列中排队抢资源
doSignal(first);
}
private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null)
//等于null意味着循环到此时条件队列已经空了,那么把lastWaiter也置为null
lastWaiter = null;
//断开notEmpty条件队列中当前节点的nextWaiter指针,也就相当于剔除当前节点,等待GC
first.nextWaiter = null;
} while (!transferForSignal(first) &&
//如果当前节点已经不是CONDITION状态的话(就说明当前节点已经失效了),就选择下一个节点尝试放进CLH队列中
(first = firstWaiter) != null);
}
/**
* 将notEmpty条件队列中的节点从条件队列移动到CLH队列当中
* 第304行代码处:
*/
final boolean transferForSignal(Node node) {
/*
如果notEmpty条件队列中的节点已经不是CONDITION状态的时候,就直接返回false,
跳过该节点,相当于把该节点剔除出条件队列
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//走到这里说明该节点的状态已经被修改成了初始状态0。把其加入到CLH队列尾部,并返回前一个节点
Node p = enq(node);
int ws = p.waitStatus;
/*
再来复习一下,SIGNAL状态表示当前节点是阻塞状态的话,上一个节点就是SIGNAL。notEmpty条件队列中的
节点此时还是处于阻塞状态,所以此时将这个节点移动到CLH队列后就需要将前一个节点的状态改为SIGNAL
如果CAS修改失败了的话,就将这个节点所在的线程唤醒去竞争锁资源,结局肯定是没抢到(因为锁资源是
当前线程所持有着),所以会在acquireQueued方法中继续被阻塞住的,而且在这其中会再次修正前一个节点
的SIGNAL状态(必定是要修改成功的,如果修改不成功,就会一直在acquireQueued方法中循环去CAS修改)
当然如果前一个节点是CANCELLED状态的话,也去唤醒这个节点。这样acquireQueued方法中有机会去剔除掉
这些CANCELLED节点,相当于做了次清理工作
需要提一下的是,该处是唤醒在第40行代码处被阻塞住的take线程(之前数组一直是空的,现在添加了一个节点
后数组就不为空了,所以需要唤醒之前被阻塞住的一个拿取线程。假设这个被唤醒的线程是线程2,执行唤醒动作
的是线程1)。如前面所说,线程2会进入到acquireQueued方法中再次被阻塞住。直到线程1走到第21行代码处:
也就是put方法中的最后一步unlock解锁的时候会被再次唤醒(也不一定就是这次会被唤醒,也有可能唤醒的是
其他的线程(假如说是线程3)。但只要线程3最后执行unlock方法的时候,就会继续去唤醒,相当于把这个
唤醒的动作给传递下去了。那么线程2最终就会有机会被唤醒(等到它变成CLH队列中的第一个节点的时候))
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
3.3 take方法
/**
* ArrayBlockingQueue:
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//响应中断模式下的加锁
lock.lockInterruptibly();
try {
while (count == 0)
//如果数组为空的话,就在notEmpty中入队一个新节点,并阻塞当前线程
notEmpty.await();
//删除数组元素并唤醒notFull
return dequeue();
} finally {
//解锁
lock.unlock();
}
}
/**
* 第13行代码处:
*/
private E dequeue() {
final Object[] items = this.items;
//记录旧值并最终返回出去
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
//将数组元素清空
items[takeIndex] = null;
//takeIndex记录的是下次拿取的位置。如果takeIndex已经是最后一个了,重新复位为0
if (++takeIndex == items.length)
takeIndex = 0;
//当前数组中的数量-1
count--;
//elementDequeued方法在数组中移除数据时会被调用,以保证Itrs迭代器和队列数据的一致性
if (itrs != null)
itrs.elementDequeued();
/*
如果notFull条件队列不为空的话,唤醒notFull条件队列中的第一个节点去CLH队列当中去排队抢资源
如果notFull里没有节点的话,说明此时数组没满。signal方法将不会有任何作用,因为此时没有阻塞住的put线程
*/
notFull.signal();
return x;
}
原创不易,未得准许,请勿转载,翻版必究