目录
1 简介
2 CLH队列
2.1 独占模式
2.2 共享模式
3 条件队列
Java版本:8u261。
AQS全称AbstractQueuedSynchronizer,是一个能多线程访问共享资源的同步器框架。作为Doug Lea大神设计出来的又一款优秀的并发框架,AQS的出现使得Java中终于可以有一个通用的并发处理机制。并且可以通过继承它,实现其中的方法,以此来实现想要的独占模式或共享模式,抑或是阻塞队列也可以通过AQS来很简单地实现出来。
一些常用的并发工具类底层都是通过继承AQS来实现的,比如:ReentrantLock、Semaphore、CountDownLatch、ArrayBlockingQueue等(这些工具类也都是Doug Lea写的)。AQS中有几个重要的模块:
AQS定义资源的访问方式有两种:
而上面所说的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已经实现好了。
这里需要注意的一点是,head指针永远会指向一个空节点。如果当前节点被剔除掉,而后面的节点变成第一个节点的时候,此时就会清空该节点里面的内容(waitStatus不会被清除),将head指针指向它。这样做的目的是为了方便进行判断。
独占模式就是只有一个线程能获取到锁资源,独占模式用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();
}
//...
}
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(需要使用者
在调用lock方法后首先通过isInterrupted方法去进行判断,是否应该执行接下来的业务代码),而不是会抛异常
*/
selfInterrupt();
}
/**
* ReentrantLock:
* 尝试获取资源
*/
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 (; ; ) {
/*
当前节点的前一个节点,注意,如果当前CLH队列中就一个节点(也就是当前节点)的话,
该方法是会抛出空指针异常的。这样就会进入到下面finally子句中的cancelAcquire
方法中做最终的释放动作
*/
final Node p = node.predecessor();
/*
如果前一个节点是头节点,才可以尝试获取资源,也就是实际上的CLH队列中的第一个节点
队列中只有第一个节点才有资格去尝试获取锁资源(FIFO),如果获取到了就不用被阻塞了
获取到了说明在此刻,之前的资源已经被释放了
*/
if (p == head && tryAcquire(arg)) {
/*
头指针指向当前节点,意味着该节点将变成一个空节点(头节点永远会指向一个空节点)
因为在上一行的tryAcquire方法已经成功的情况下,就可以释放CLH队列中的该节点了
*/
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
/*
如果前一个节点不是头节点,或者没有获取到锁资源的时候,将队列中当前节点之前的一些
CANCELLED状态的节点剔除。然后前一个节点状态如果为SIGNAL时,就会阻塞当前线程
这里的parkAndCheckInterrupt阻塞操作是很有意义的。因为如果不阻塞的话,
那么获取不到资源的线程可能会在这个死循环里面一直运行,会一直占用CPU资源
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//只是记录一个标志位而已,不会抛出InterruptedException异常。也就是说不会响应中断
interrupted = true;
}
} finally {
if (failed)
//如果CLH队列当前就一个节点,或者tryAcquire方法中state+1溢出了,就会取消当前线程获取锁资源的请求
cancelAcquire(node);
}
}
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;
}
/**
* 第154行代码处:
* 阻塞当前节点,后续该节点如果被unpark唤醒的时候,会从第203行代码处唤醒往下执行,返回false
* 可能线程在等待的时候会被中断唤醒,本方法就返回了true。这个时候该线程就会处于一种不正确的状态
* 返回回去后会在第156行代码处设置中断位为true,然后走到cancelAcquire方法中取消锁资源,并最终返
* 回到了第27行代码处。注意到下面的第204行代码处使用的是Thread.interrupted方法,也就是在返回
* true之后会清空中断状态,所以需要在上面的第32行代码处调用selfInterrupt方法里面的interrupt方法来
* 将中断标志位重新置为true
*/
private final boolean parkAndCheckInterrupt() {
//当前线程会被阻塞到这行代码处,停止往下运行,等待unpark唤醒
LockSupport.park(this);
return Thread.interrupted();
}
/**
* 第161行代码处:
* 取消当前线程获取锁资源的请求,并完成一些其他的收尾工作
*/
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;
}
}
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:
*/
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;
}
/**
* AbstractQueuedSynchronizer:
* 第22行代码处:
* 唤醒下一个可以被唤醒的节点
*/
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队列的尾节点向前遍历到该节点为止,找到该节点往后第一个处于正常阻塞状态的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果找到了或者遍历之前的下一个节点本身就处于正常阻塞状态的话,就唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
共享模式就是有多个线程可以同时拿到锁资源,共享模式用Semaphore来举例,其与ReentrantLock的结构类似,也有公平和非公平两种模式:
public class Semaphore implements Serializable {
//...
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
//...
}
static final class NonfairSync extends Sync {
//...
}
static final class FairSync extends Sync {
//...
}
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。
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:
*/
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 (; ; ) {
/*
当前节点的前一个节点,注意,如果当前CLH队列中就一个节点(也就是当前节点)的话,
该方法是会会抛出空指针异常的。这样就会进入到下面finally子句中的cancelAcquire
方法中做最终的释放动作
*/
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)
//如果CLH队列当前就一个节点,或者线程被中断后唤醒,就会取消当前线程获取锁资源的请求
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
/*
propagate>0表示还有剩余锁资源;head指针为null或者头节点的后面一个节点(即真正第一个等待的节点)是阻塞状态
又或者head节点本身就是PROPAGATE状态,在这些条件满足后调用doReleaseShared方法来唤醒后面的节点,因为是
共享状态,锁可以有多个线程所持有
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//node是最后一个节点或者node的下一个节点是共享节点的时候才去唤醒
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方法会有多个线程在调用
假如此时有多个线程进入到第122行代码处,而其中一个线程先执行了第123行代码处的if条件,
将头节点状态改为了0。而剩下的线程就不能跳进第123行代码处的if条件中,而只能走到第132行代码处
ws == 0条件满足,剩下的线程于是就去CAS竞争修改头节点状态为PROPAGATE。修改成功的那个线程就跳到了
第151行代码处,进行下个判断逻辑,而再剩下的那些线程就让它们继续循环就行了
修改为PROPAGATE状态,表示需要将状态向后继结点传播,直到遇到一个独占节点停止
*/
continue;
}
/*
如果head节点有变动的话,就继续循环。也就是说在本方法内部,有其他的线程已经释放了头节点
说白了就是上面的unparkSuccessor方法唤醒了之后的一个节点并抢到了资源,改变了head节点。在这里进行判断头节点
已经改动过了,于是会再次循环去唤醒,直到所有被唤醒的节点都被唤醒了,或者遇到一个独占节点,head节点不再变动为止
*/
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:
*/
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;
}
}
因为CLH队列中的线程,什么线程获取到锁,什么线程进入队列排队,什么线程释放锁,这些都是不受我们控制的。所以条件队列的出现为我们提供了主动式地、只有满足指定的条件后才能线程阻塞和唤醒的方式。对于条件队列首先需要说明一些概念:条件队列是AQS中除了CLH队列之外的另一种队列,每创建一个Condition实际上就是创建了一个条件队列,而每调用一次await方法实际上就是往条件队列中入队,每调用一次signal方法实际上就是往条件队列中出队。不像CLH队列上节点的状态有多个,条件队列上节点的状态只有一个:CONDITION。所以如果条件队列上一个节点不再是CONDITION状态时,就意味着这个节点该出队了。需要注意的是,条件队列只能运行在独占模式下。
一般在使用条件队列作为阻塞队列来使用时都会创建两个条件队列:notFull和notEmpty。notFull表示当条件队列已满的时候,put方法会处于等待状态,直到队列没满;notEmpty表示当条件队列为空的时候,take方法会处于等待状态,直到队列有数据了。
而notFull.signal方法和notEmpty.signal方法会唤醒各自条件队列上的节点,这些节点会被放进CLH队列中,争抢锁资源。也就是说,存在一个节点从条件队列被转移到CLH队列的情况发生。同时也意味着,条件队列上不会发生锁资源竞争,所有的锁竞争都是发生在CLH队列上的。
其他一些条件队列和CLH队列之间的差异如下:
下面就是具体的源码分析了。条件队列以ArrayBlockingQueue的非公平锁方式来举例。ArrayBlockingQueue的构造方法:
/**
* 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来实现
lock = new ReentrantLock(fair);
//notEmpty条件队列
notEmpty = lock.newCondition();
//notFull条件队列
notFull = lock.newCondition();
}
ArrayBlockingQueue的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:
*/
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队列尾,并退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
/*
走到这里说明当前节点已经插入到了CLH队列中。然后在CLH队列中进行获取锁资源的操作
*/
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
/*
<<>>
之前分析过的如果acquireQueued方法返回true,说明当前线程被中断了
返回true意味着在acquireQueued方法中此时会再一次被中断(注意,这意味着有两个代码点判断线程是否被中断:
一个是在第40行代码处,另一个是在acquireQueued方法里面),如果之前没有被中断,则interruptMode=0,
而在acquireQueued方法里面线程被中断返回了,这个时候将interruptMode重新修正为REINTERRUPT即可
至于为什么不修正为THROW_IE是因为在这种情况下,第38行代码处已经通过调用signal方法正常唤醒了,
节点已经放进了CLH队列中。而此时的中断是在signal操作之后,在第46行代码处去抢锁资源的时候发生的
这个时候中断不中断已经无所谓了,所以就不需要抛出InterruptedException
*/
interruptMode = REINTERRUPT;
/*
走到这里说明当前节点已经获取到了锁资源(获取不到的话就会被再次阻塞在acquireQueued方法里)
如果interruptMode=REINTERRUPT的话,说明之前已经调用过signal方法了,也就是说该节点已经从条件队列中剔除掉了,
nextWaiter指针肯定为空,所以在这种情况下是不需要执行unlinkCancelledWaiters方法的
而如果interruptMode=THROW_IE的话,说明之前还没有调用过signal方法来从条件队列中剔除该节点。这个时候就需要调用
unlinkCancelledWaiters方法来剔除这个节点了(在之前的transferAfterCancelledWait方法中
已经把该节点的状态改为了初始状态0),顺便把所有其他不是CONDITION状态的节点也一并剔除掉
*/
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//根据不同模式处理中断(正常模式不需要处理)
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
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;
}
/**
* 删除条件队列当中所有不是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;
}
}
/**
* 第34行代码处:
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
/*
释放锁资源。注意这里是释放所有的锁,包括可重入锁有多次加锁的话,会一次性全部释放。因为在上一行
代码savedState存的是所有的锁资源,而这里就是释放这些所有的资源,这也就是方法名中“fully”的含义
*/
if (release(savedState)) {
failed = false;
return savedState;
} else {
/*
释放失败就抛异常,也就是说没有释放干净,可能是在并发的情景下state被修改了的原因,
也可能是其他原因。注意如果在这里抛出异常了那么会走第160行代码
*/
throw new IllegalMonitorStateException();
}
} finally {
/*
如果释放锁失败,就把节点置为CANCELLED状态。比较精妙的一点是,在之前第80行代码处,
判断条件队列中是否有不是CONDITION的节点时,只需要判断最后一个节点的状态是否是CONDITION就行了
按常理来说,是需要遍历整个队列才能知道的。但是条件队列每次添加新节点都是插在尾处,而如果释放锁失败,
会将这个新添加的、在队列尾巴的新节点置为CANCELLED状态。而之前的CONDITION节点必然都是在队头
因为如果此时再有新的节点入队的话,会首先在第82行代码处将所有不是CONDITION的节点都剔除了
也就是说无论什么情况下,如果队列中有不是CONDITION的节点,那它一定在队尾,所以只需要判断它就可以了
*/
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
/**
* 第37行和第237行代码处:
* 判断节点是否在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;
}
}
/**
* 第40行代码处:
* 如果当前线程没有被中断过,则返回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方法
*
* THROW_IE:表示在线程中断发生时还没有调用过signal方法,这个时候我们将这个节点放进CLH队列中去抢资源,
* 直到抢到锁资源后,再把这个节点从CLH队列和条件队列中都删除掉,最后再抛出InterruptedException
*
* 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;
}
/**
* 第71行代码处:
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
//如果是THROW_IE最终就会抛出InterruptedException异常
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
//如果是REINTERRUPT就仅仅是中断当前线程而已
selfInterrupt();
}
/**
* ArrayBlockingQueue:
* 第17行代码处:
*/
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;
//断开当前节点的nextWaiter指针,也就相当于剔除当前节点,等待GC
first.nextWaiter = null;
} while (!transferForSignal(first) &&
//如果当前节点已经不是CONDITION状态的话(就说明当前节点已经失效了),就选择下一个节点尝试放进CLH队列中
(first = firstWaiter) != null);
}
/**
* 将当前节点从条件队列移动到CLH队列当中
*/
final boolean transferForSignal(Node node) {
//如果当前节点已经不是CONDITION状态的时候,就直接返回false,跳过该节点,相当于把该节点剔除出条件队列
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//走到这里说明该节点的状态已经被修改成了初始状态0。把其加入到CLH队列尾部,并返回前一个节点
Node p = enq(node);
int ws = p.waitStatus;
/*
再来复习一下,SIGNAL状态表示当前节点是阻塞状态的话,上一个节点就是SIGNAL。当前节点此时还是处于阻塞状态,
所以此时将当前节点移动到CLH队列后就需要将前一个节点的状态改为SIGNAL。如果CAS修改失败了的话,
就将当前线程唤醒去竞争锁资源,如果没抢到的话无妨,acquireQueued方法中会去继续被阻塞住的,
而且会再次修正前一个节点的SIGNAL状态。当然如果前一个节点是CANCELLED状态的话,也去唤醒当前节点
这样acquireQueued方法中有机会去剔除掉这些CANCELLED节点,相当于做了次清理工作
需要提一下的是,该处唤醒线程是唤醒在第38行代码处阻塞住的线程
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
ArrayBlockingQueue的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();
}
}
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;
}