AQS特点
- 首先AQS提供了一个带头尾节点的双向链表实现的 First In First Out 队列,每个节点都是Node节点,这个队列是构建锁及其他同步机制的基础框架,condition queue 不是必须的,只有用到了condition,才会有condition队列,condition队列可以有多个;
- 利用一个int类型来表示状态(status),例如基于AQS实现的ReentrantLock status=0表示没加锁,status=1表示加锁,status>1表示重入锁;
- 使用方法是继承,AQS使用的是模板方法,子类需要继承AQS并复写其中的方法;
- 子类通过继承并通过实现它的方法来管理状态,例如acquire和release方法来操纵状态;
- 可以实现共享锁和排他锁(独占模式、共享模式)
AQS是实现思路
AQS 内部维护了一个 CLH 同步队列。 ,AQS 依赖它来完成同步状态的管理,线程首先会尝试获取锁,如果失败时,AQS 则会将当前线程等待状态及等待信息构造成一个节点(Node)并将其加入到 CLH 同步队列, 线程会再次尝试获取锁,前提是当前节点是head的后继节点,如果获取失败,则将自己阻塞,直到被唤醒。已经获取锁的线程,如果释放锁,会唤醒队列后继线程,基于这些设计及思路,JDK提供了很多基于AQS的子类。
CLH 同步队列是一个 FIFO 双向队列
在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)
AQS是怎么实现的呢?
JDK1.5以前只有synchronized同步锁,并且效率非常低,因此大神Doug Lea自己写了一套并发框架,这套框架的核心就在于AbstractQueuedSynchronizer类(即AQS),性能非常高,所以被引入JDK包中,即JUC。
本篇就是对AQS及其相关组件进行分析,了解其原理,并领略大神的优美而又精简的代码。
AbstractQueuedSynchronizer简称AQS
【ReentrantLock使用示例】
private Lock lock = new ReentrantLock();
public void test(){
lock.lock();
try{
doSomeThing();
}catch (Exception e){
// ignored
}finally {
lock.unlock();
}
}
【AQS】
是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。AQS内部是使用了双向链表将等待线程链接起来,当发生并发竞争的时候,就会初始化该队列并让线程进入睡眠等待唤醒,同时每个节点会根据是否为共享锁标记状态为共享模式或独占模式。
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
AbstractQueuedSynchronizer
AQS是JUC下最核心的类,没有之一,所以我们先来分析一下这个类的数据结构。
AbstractQueuedSynchronizer 重要属性详情
/**
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 7373984972572414691L;
/**
* 不能序列化的字段,等待队列的头结点,它不与任何线程关联,延迟初始化。除初始化外,只能通过setHead方法进行修改。
* 注意:如果head存在,则保证其waitStatus不为CANCELLED(表示这个节点的线程已取消获取锁的请求) 状态。
*/
private transient volatile Node head;
/** 不能序列化的字段,等待队列的尾节点,延迟初始化。仅通过enq()方法进行修改,添加新的等待节点 */
private transient volatile Node tail;
/** 同步状态,大于0表示有线程持有锁,大于1表示线程重入,等于0表示锁未被线程占有 */
private volatile int state;
}
父类AbstractOwnableSynchronizer属性:exclusiveOwnerThread
/**
* @since 1.6
* @author Doug Lea
*/
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
/** 不能序列化的字段,记录当前持有独占锁的线程 */
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
AbstractQueuedSynchronizer 内部类Node 源码详情
static final class Node {
/** 共享节点 */
static final Node SHARED = new Node();
/** 独占节点 */
static final Node EXCLUSIVE = null;
/** waitStatus状态,表示这个节点的线程已取消获取锁的请求 */
static final int CANCELLED = 1;
/** waitStatus状态,表示当前节点存在有效的后继节点,当前节点释放锁后需要唤醒后继节点 */
static final int SIGNAL = -1;
/** waitStatus状态,表示当前节点处于条件队列中*/
static final int CONDITION = -2;
/** waitStatus状态,共享锁模式下需要进行传播唤醒后继共享节点 */
static final int PROPAGATE = -3;
/** 节点状态 */
volatile int waitStatus;
/** 前置节点 */
volatile Node prev;
/** 后置节点 */
volatile Node next;
/** 节点关联的线程 */
volatile Thread thread;
/** 条件队列中下一个等待的线程 */
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回上一个节点,如果为null,则抛出NullPointerException。
* 在前置节点不能为null时使用。
* 可以忽略空检查,但是它可以帮助VM。
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
/** 用于建立初始标头或SHARED标记 */
Node() {}
/** 入队的addWaiter过程使用 */
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
/** 用于条件入队addConditionWaiter()使用 */
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AbstractQueuedSynchronizer 内部类ConditionObject 属性
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** 不能序列化的字段,条件队列的第一个节点 */
private transient Node firstWaiter;
/** 不能序列化的字段,条件队列的最后一个节点 */
private transient Node lastWaiter;
/** 构造函数 */
public ConditionObject() { }
}
Lock
Lock是一个接口,提供了加/解锁的通用API,JUC主要提供了两种锁,ReentrantLock和ReentrantReadWriteLock,前者是重入锁,实现Lock接口,后者是读写锁,本身并没有实现Lock接口,而是其内部类ReadLock或WriteLock实现了Lock接口。
先来看看Lock都提供了哪些接口:
// 普通加锁,不可打断;未获取到锁进入AQS阻塞
void lock();
// 可打断锁
void lockInterruptibly() throws InterruptedException;
// 尝试加锁,未获取到锁不阻塞,返回标识
boolean tryLock();
// 带超时时间的尝试加锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 解锁
void unlock();
// 创建一个条件队列
Condition newCondition();
ReentrantLock
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现
synchronized和ReentrantLock都是可重入的,后者使用更加灵活,也提供了更多的高级特性,但其本质的实现原理是差不多的(synchronized 在jdk1.6以后优化很多都是借鉴了ReentrantLock的实现原理,就是努力在用户态把加锁问题解决,避免进入内核态的线程阻塞)。
public class ReentrantLock implements Lock, Serializable {
/** 无参构造默认则是非公平锁 */
public ReentrantLock() {
sync = new NonfairSync();
}
/** 有参构造是根据参数创建true=公平锁,或false=非公平锁 */
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
非公平锁/公平锁
lock
非公平锁和公平锁在实现上基本一致,只有个别的地方不同,因此下面会采用对比分析方法进行分析。
从lock()方法开始:
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.lock();
}
}
实际上是委托给了内部类Sync,该类实现了AQS(其它组件实现方法也基本上都是这个套路);由于有公平和非公平两种模式,因此该类又实现了两个子类:FairSync和NonfairSync:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
...
}
}
NonfairSync :
/**
* 非公平锁
*/
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
非公平锁首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,直接将exclusiveOwnerThread设置为当前线程,设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。
非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
/**
* 公平锁
*/
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
区别: 这里就是 公平锁和非公平锁的第一个不同,非公平锁首先会调用CAS将state从0改为1,如果能改成功则表示获取到锁,直接将exclusiveOwnerThread设置为当前线程,不用再进行后续操作;否则则同公平锁一样调用acquire方法获取锁,这个是在AQS中实现的模板方法:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
**tryAcquire()**这里两种锁唯一不同的实现就是tryAcquire方法,先来看非公平锁的实现:
1.第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。如果失败则调用acquireQueued入队
非公平锁tryAcquire()
/**
* 非公平锁
*/
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state变量值
int c = getState();
if (c == 0) { //没有线程占用锁
if (compareAndSetState(0, acquires)) {
//占用锁成功,设置独占线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { //判断是否是当前线程占用该锁
//当前线程占用锁,则state+1,表示重入次数+1并返回true,加锁成功
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新state值为新的重入次数
setState(nextc);
return true;
}
//获取锁失败
return false;
}
}
state=0表示还没有被线程持有锁,直接通过CAS修改,能修改成功的就获取到锁,修改失败的线程先判断exclusiveOwnerThread是不是当前线程,是则state+1,表示重入次数+1并返回true,加锁成功,否则则返回false表示尝试加锁失败并调用acquireQueued入队。
公平锁tryAcquire()
/**
* 公平锁
*/
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state变量值
int c = getState();
if (c == 0) {//没有线程占用锁
//这里跟非公平锁区别是:首次加锁需要判断是否已经有队列存在,没有才去加锁,有则直接返回false
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//占用锁成功,设置独占线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//判断是否是当前线程占用该锁
//当前线程占用锁,则state+1,表示重入次数+1并返回true,加锁成功
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 更新state值为新的重入次数
setState(nextc);
return true;
}
return false;
}
}
可以看到公平锁和非公平锁的代码基本是一样的,区别在于首次加锁需要判断是否已经有队列存在,没有才去加锁,有则直接返回false。
addWaiter入队
2.第二步,入队。如果已经有线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。
先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)方法,当尝试加锁失败时,首先就会调用该方法创建一个Node节点并添加到队列中去。
/**
* 将新节点和当前线程关联并且入队列
* @param mode 独占/共享
* @return 新节点
*/
private Node addWaiter(Node mode) {
//初始化节点,设置关联线程和模式(独占 or 共享)
Node node = new Node(Thread.currentThread(), mode);
// 获取尾节点引用
Node pred = tail;
// 尾节点不为空,说明队列已经初始化过
if (pred != null) {
node.prev = pred;
// 设置新节点为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
enq(node);
return node;
}
这里首先传入了一个独占模式的空节点,并根据该节点和当前线程创建了一个Node,然后判断是否已经存在队列,若存在则直接入队,否则调用enq方法初始化队列,提高效率。
/**
* 初始化队列并且入队新节点
*/
private Node enq(final Node node) {
//开始自旋
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 如果tail为空,则新建一个head节点,并且tail指向head
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// tail不为空,将新节点入队
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。当B、C入等待队列后,此时AQS队列如下:
此处还有一个非常细节的地方,为什么设置尾节点时都要先将Node节点的前置节点设置为tail尾结点呢,而不是在CAS之后再设置?
比如像下面这样:
if (compareAndSetTail(tail, node)) {
node.prev = tail;
tail.next = node;
return node;
}
因为如果这样做的话,在CAS设置完tail后会存在一瞬间的tail.pre=null的情况,而Doug Lea正是考虑到这种情况,不论何时获取tail.pre都不会为null。
- 第三步,挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。
/**
* 已经入队的线程尝试获取锁
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //标记是否成功获取锁
try {
boolean interrupted = false; //标记线程是否被中断过
for (;;) {
final Node p = node.predecessor(); //获取前驱节点
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功,将当前节点设置为head节点
p.next = null; // 原head节点出队,在某个时间点被GC回收
failed = false; //获取成功
return interrupted; //返回是否被中断过
}
// 判断获取失败后是否可以挂起,若可以则挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程若被中断,设置interrupted为true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
code里的注释已经很清晰的说明了acquireQueued的执行流程。假设线程B和C在竞争锁的过程中线程A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。我们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧
/**
* 判断当前线程获取锁失败之后是否需要挂起.
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱节点状态为signal,返回true
return true;
// 前驱节点状态为CANCELLED
if (ws > 0) {
// 从队尾向前寻找第一个状态不为CANCELLED的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱节点的状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* 挂起当前线程,返回线程中断状态并重置
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。
最终队列可能会如下图所示
park细节
为什么在park前需要这么一个判断呢?因为当前节点的线程进入park后只能被前一个节点唤醒,那前一个节点怎么知道有没有后继节点需要唤醒呢?
// 判断获取失败后是否可以挂起,若可以则挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程若被中断,设置interrupted为true
interrupted = true;
因此当前节点在park前需要给前一个节点设置一个标识,即将waitStatus设置为Node.SIGNAL(-1),然后自旋一次再走一遍刚刚的流程,若还是没有获取到锁,则调用parkAndCheckInterrupt进入睡眠状态。
打断
读者可能会比较好奇Thread.interrupted这个方法是做什么用的。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个是用来判断当前线程是否被打断过,并清除打断标记(若是被打断过则会返回true,并将打断标记设置为false),所以调用lock方法时,通过interrupt也是会打断睡眠的线程的,只是Doug Lea做了一个假象,让用户无感知。
但有些场景又需要知道该线程是否被打断过,所以acquireQueued最终会返回interrupted打断标记,如果是被打断过,则返回的true,并在acquire方法中调用selfInterrupt再次打断当前线程(将打断标记设置为true)。
lockInterruptibly
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
可以看到区别就在于使用lockInterruptibly加锁被打断后,是直接抛出InterruptedException异常,我们可以捕获这个异常进行相应的处理。
取消
最后来看看cancelAcquire是如何取消加锁的,该情况比较特殊,简单了解下即可:
private void cancelAcquire(Node node) {
if (node == null)
return;
// 首先将线程置空
node.thread = null;
// waitStatus > 0表示节点处于取消状态,则直接将当前节点的pre指向在此之前的最后一个有效节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 保存前一个节点的下一个节点,如果在此之前存在取消节点,这里就是之前取消被取消节点的头节点
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
// 当前节点是tail节点,则替换尾节点,替换成功则将新的尾结点的下一个节点设置为null;
if (node == tail && compareAndSetTail(node, pred)) {
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) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
unlock
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//先尝试释放锁
if (tryRelease(arg)) {
Node h = head;
//查看头结点的状态是否为SIGNAL
if (h != null && h.waitStatus != 0)
//是则唤醒头结点的下个节点关联的线程
unparkSuccessor(h);
return true;
}
return false;
}
如果理解了加锁的过程,那么解锁看起来就容易多了。流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。
最后我们再看下tryRelease的执行过程
/**
* 释放当前线程占用的锁
* @param releases
* @return 是否释放成功
*/
protected final boolean tryRelease(int releases) {
// 计算释放后state值,当前state-1
int c = getState() - releases;
// 如果不是当前线程占用锁,那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果state==0,则表示完全释放锁;
if (c == 0) {
// 锁被重入次数为0,表示释放成功
free = true;
// 清空独占线程
setExclusiveOwnerThread(null);
}
// 更新state值
setState(c);
return free;
}
这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。
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);
}
解锁就比较简单了,先调用tryRelease对state执行减一操作,如果state==0,则表示完全释放锁;若果存在后继节点,则调用unparkSuccessor唤醒后继节点,唤醒后的节点的waitStatus会重新被设置为0。
只是这里有一个小细节,为什么是从后向前找呢?因为我们在开始说过,设置尾节点保证了node.pre不会为null,但pre.next仍有可能是null,所以这里只能从后向前找到最后一个有效节点。
用一张流程图总结一下非公平锁的获取锁的过程。
超时机制
在ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。那么超时的功能是怎么实现的呢?我们还是用非公平锁为例来一探究竟。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
还是调用了内部类里面的方法。我们继续向前探究
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果线程被中断了,那么直接抛出InterruptedException
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
这里的语义是:如果线程被中断了,那么直接抛出InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。tryAcquire我们已经看过,这里重点看一下doAcquireNanos做了什么。
/**
* 在有限的时间内去竞争锁
* @return 是否获取成功
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 起始时间
long lastTime = System.nanoTime();
// 线程入队
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
// 又是自旋!
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// 如果前驱是头节点并且占用锁成功,则将当前节点变成头结点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 如果已经超时,返回false
if (nanosTimeout <= 0)
return false;
// 超时时间未到,且需要挂起
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 阻塞当前线程直到超时时间到期
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
// 更新nanosTimeout
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
//相应中断
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireNanos的流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试
ReentrantReadWriteLock
ReentrantLock是一把独占锁,只支持重入,不支持共享,所以JUC包下还提供了读写锁,这把锁支持读读并发,但读写、写写都是互斥的。
读写锁也是基于AQS实现的,也包含了一个继承自AQS的内部类Sync,同样也有公平和非公平两种模式,下面主要讨论非公平模式下的读写锁实现。
读写锁实现相对比较复杂,在ReentrantLock中就是使用的int型的state属性来表示锁被某个线程占有和重入次数,而ReentrantReadWriteLock分为了读和写两种锁,要怎么用一个字段表示两种锁的状态呢?
Doug Lea大师将state字段分为了高二字节和低二字节,即高16位用来表示读锁状态,低16位则用来表示写锁,如下图:
因为读写锁状态都只用了两个字节,所以可重入的次数最多是65535,当然正常情况下重入是不可能达到这么多的。
AQS如何用一个int值表示读写锁的2种状态?
在AQS的实现类ReentrantReadWriteLock$Sync中,将32位int类型的state分成2部分,
高位的16个2进制位表示读锁,低位的16个2进制位表示写锁。
即读锁和写锁的并发最多只能达到2的16次方减去1。
0000000000000000 0000000000000000
-----------------------------------------------------------------------------
如果是读锁进入,则变成:
0000000000000001 0000000000000000
即将原有的state值加上65535(即2的16次方-1)
如何获取读锁数量:
将state的高位右移16位,则变成:
0000000000000000 0000000000000001
--------------------------------------------------------------------------------
如果是写锁进入,则变成:
0000000000000000 0000000000000001
即将原有的state值加1
如何获取写锁数量:
将state高位并上16个0,低位并上16个1(2的16次方减1),0&0或者1始终都为0,即将高位变成0,1&1为1,
则低位并上16个1值不变
0000000000000000 0000000000000001
0000000000000000 1111 1111 1111 1111
=0000000000000000 0000000000000001
那它是怎么实现的呢?还是先从构造方法开始:
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
同样默认就是非公平锁,同时还创建了readerLock和writerLock两个对象,我们只需要像下面这样就能获取到读写锁:
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock r = lock.readLock();
private static Lock w = lock.writeLock();
写锁
由于写锁的加锁过程相对更简单,下面先从写锁加锁开始分析,入口在ReentrantReadWriteLock#WriteLock.lock()方法,点进去看,发现还是使用的AQS中的acquire方法:
/**
* 写锁
*/
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
public void lock() {
//AQS中的acquire方法
sync.acquire(1);
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//AQS中的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
tryAcquire是ReentrantReadWriteLock 内部类Sync 中实现方法
abstract static class Sync extends AbstractQueuedSynchronizer {
/*
* 读取与写入计数提取常量和函数。
* 锁定状态在逻辑上分为两个无符号的short:
* 下部16位表示排他(写)锁定保持计数,
* 上部16位表示共享(读取器)保持计数。
*/
static final int SHARED_SHIFT = 16;
//1<<16 = 65536 = 0000000000000001 0000000000000000
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// (1 << 16 )-1 = 65535 = 0000000000000000 1111111111111111
//即读锁的最大并发量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//写锁的最大并发量 0000000000000000 1111111111111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 获取读锁数量 将state的高位右移16位 */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** 获取写锁数量 将state的2进制低16位并上16个1(2的16次方减1) */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 获取写锁加锁和重入的次数
int w = exclusiveCount(c);
if (c != 0) { // 已经有线程持有锁,也许是读锁,也许是写锁
// 这里有两种情况:
//-----1. w==0表示有线程获取了读锁,不论是否是当前线程,
//直接返回false,也就是说读-写锁是不支持升级重入的(但支持写-读降级)
//-----2. w!=0 && current != getExclusiveOwnerThread(),
//有线程获取了写锁并且不是当前线程,表示有其它线程持有了写锁,写写互斥
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
// 超出65535,抛异常
throw new Error("Maximum lock count exceeded");
// 否则就是当前线程已经获取了写锁,写锁的次数直接加1
setState(c + acquires);
return true;
}
// c==0没有线程持有锁,才会走到这,但这时存在两种情况:
// -----1.公平锁时:如果队列为空或者队列第一个节点是当前线程,返回false
// 如果队列队列不为空,则返回true,表示要规规矩矩的排队
// -----2.非公平锁writerShouldBlock() 返回false,然后直接使用CAS加锁即可
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
}
ReentrantReadWriteLock.FairSync的writerShouldBlock()方法
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
//判断是否有队列
return hasQueuedPredecessors();
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
}
ReentrantReadWriteLock.NonfairSync的writerShouldBlock()方法
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
}
读锁
/**
* 读锁
*/
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
public void lock() {
//读锁在加锁开始就和其它锁不同,调用的是acquireShared方法,意为获取共享锁。
sync.acquireShared(1);
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
}
tryAcquireShared是ReentrantReadWriteLock 内部类Sync 中实现方法
abstract static class Sync extends AbstractQueuedSynchronizer {
/*
* 读取与写入计数提取常量和函数。
* 锁定状态在逻辑上分为两个无符号的short:
* 下部16位表示排他(写)锁定保持计数,
* 上部16位表示共享(读取器)保持计数。
*/
static final int SHARED_SHIFT = 16;
//1<<16 = 65536 = 0000000000000001 0000000000000000
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// (1 << 16 )-1 = 65535 = 0000000000000000 1111111111111111
//即读锁的最大并发量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//写锁的最大并发量 0000000000000000 1111111111111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 获取读锁数量 将state的高位右移16位 */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** 获取写锁数量 将state的2进制低16位并上16个1(2的16次方减1) */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
/**
* 1.如果写锁被另外一个线程持有,则返回-1(读写互斥)
* 2.如果获取到了锁,则返回1
* 3.步骤2失败,则循环重试
**/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//为什么读写互斥?因为读锁一上来就判断了是否有其它线程持有了写锁
//(当前线程持有写锁再获取读锁是可以的)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
//如果写锁被另外一个线程持有,则返回-1
return -1;
int r = sharedCount(c);
//------1.公平锁时:如果队列为空或者队列第一个节点是当前线程,返回false
//如果队列队列不为空,则返回true,表示要规规矩矩的排队
//------2.非公平锁时:如果队列为空或者前面没有写锁在排队时,则返回false
//如果头结点不为空,下一个结点也不为空并且是写锁时,返回true,表示要规规矩矩的排队
// 返回false则需要判断读锁加锁次数是否超过65535,没有则使用CAS给读锁+1
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) { //没有线程占用读锁
// 第一个读锁线程就是当前线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 记录读锁的重入
firstReaderHoldCount++;
} else {
// 获取最后一次加读锁的重入次数记录器HoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//当前线程第一次重入需要初始化,以及当前线程和缓存的最后一次记录器的
//线程id不同,需要从ThreadLocalHoldCounter拿到对应的记录器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
// 缓存到ThreadLocal
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
}
公平锁:ReentrantReadWriteLock.FairSync的readerShouldBlock()方法
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//公平锁时:如果队列为空或者队列第一个节点是当前线程,返回false
//如果队列队列不为空,则返回true,表示要规规矩矩的排队
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
}
非公平锁:ReentrantReadWriteLock.NonfairSync 的readerShouldBlock()方法
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
//非公平锁时:如果队列为空或者前面没有写锁在排队时,则返回false
//如果头结点不为空,下一个结点也不为空并且是写锁时,返回true,表示要规规矩矩的排队
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
}
这段代码有点复杂,首先需要保证读写互斥,然后进行初次加锁,若加锁失败就会调用fullTryAcquireShared方法进行兜底处理。在初次加锁中与写锁不同的是,写锁的state可以直接用来记录写锁的重入次数,因为写写互斥,但读锁是共享的,state用来记录读锁的加锁次数了,重入次数该怎么记录呢?
重入是指同一线程,那么是不是可以使用ThreadLocl来保存呢?没错,Doug Lea就是这么处理的,新增了一个HoldCounter类,这个类只有线程id和重入次数两个字段,当线程重入的时候就会初始化这个类并保存在ThreadLocalHoldCounter类中,这个类就是继承ThreadLocl的,用来初始化HoldCounter对象并保存。
这里还有个小细节,为什么要使用cachedHoldCounter缓存最后一次加读锁的HoldCounter?
因为大部分情况下,重入和释放锁的线程很有可能就是最后一次加锁的线程,所以这样做能够提高加解锁的效率,Doug Lea真是把性能优化到了极致。
上面只是初次加锁,有可能会加锁失败,就会进入到fullTryAcquireShared方法:
final int fullTryAcquireShared(Thread current) {
/**
* 该代码与tryAcquireShared中的代码部分冗余,
* 但由于不使tryAcquireShared与重试和延迟读取保持计数之间的交互复杂化,因此整体代码更简单。
*
**/
HoldCounter rh = null;
for (;;) { // 无限循环
// 获取状态
int c = getState();
if (exclusiveCount(c) != 0) { // 写线程数量不为0
if (getExclusiveOwnerThread() != current) // 不为当前线程
return -1;
} else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) { // 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
} else { // 当前线程不为第一个读线程
if (rh == null) { // 计数器不为空
//获取最后一次加读锁的重入次数记录器HoldCounter
rh = cachedHoldCounter;
// 计数器为空或者计数器的tid不为当前正在运行的线程的tid
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT) // 读锁数量为最大值,抛出异常
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // CAS比较并且设置成功
if (sharedCount(c) == 0) { // 读线程数量为0
// 设置第一个读线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//设置读锁重入次数
firstReaderHoldCount++;
} else {
if (rh == null)
//获取最后一次加读锁的重入次数记录器HoldCounter
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//当前线程第一次重入需要初始化,以及当前线程和缓存的最后一次记录器的
//线程id不同,需要从ThreadLocalHoldCounter拿到对应的记录器
rh = readHolds.get();
else if (rh.count == 0)
//缓存到ThreadLocal
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
这个方法中代码和tryAcquireShared基本上一致,只是采用了自旋的方式,处理初次加锁中的漏网之鱼
上面两个方法若返回大于0则表示加锁成功,小于0则会调用doAcquireShared方法,这个就和之前分析的acquireQueued差不多了:
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 以共享的不间断模式进行获取锁。
*/
private void doAcquireShared(int arg){
// 1. 将当前的线程封装成 Node 加入到队列Sync Queue里面
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for(;;){
/** 2. 获取当前节点的前继节点 (当一个n在 Sync Queue 里面, 并且没有获取
* lock 的 node 的前继节点不可能是 null)
**/
final Node p = node.predecessor();
if(p == head){
/**
* 3. 判断前继节点是否是head节点(前继节点是head, 存在两种情况 (1) 前
* 继节点现在占用 lock (2)前继节点是个空节点, 已经释放 lock, node
* 现在有机会获取 lock); 则再次调用 tryAcquireShared 尝试获取一下
**/
int r = tryAcquireShared(arg);
if(r >= 0){
// 4. 获取lock成功, 设置新的 head, 并唤醒后继获取readLock的节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
/**
* 5. 在获取 lock 时, 如果被中断过, 则自己再自我中断一下
* (外面的函数可能需要这个参数)
**/
if(interrupted){
selfInterrupt();
}
failed = false;
return;
}
}
/**
* 6.调用 shouldParkAfterFailedAcquire判断是否需要中断(这里可能会
* 一开始返回 false, 但在此进去后直接返回 true(主要和前继节点的状态是
* 否是 signal有关))
**/
/**
* 7. 现在lock还是被其他线程占用 那就睡一会,
* parkAndCheckInterrupt返回值判断是否这次线程的唤醒是被中断唤醒
**/
if(shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()){
interrupted = true;
}
}
}finally {
// 8. 在整个获取中出错(比如线程中断/超时)
if(failed){
// 9.清除node节点(清除的过程是先给 node 打上 CANCELLED标志, 然后再删除)
cancelAcquire(node);
}
}
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//两个入参,node是当前成功获取共享锁的节点,
//propagate就是tryAcquireShared方法(尝试加锁,成功返回1,失败返回-1,也有可能返回0)
//的返回值,注意上面说的,它可能大于0也可能等于0
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; //记录当前头节点
//设置新的头节点,即把当前获取到锁的节点设置为头节点
//注:这里是获取到锁之后的操作,不需要并发控制
setHead(node);
//这里意思有两种情况是需要执行唤醒操作
//1.propagate > 0 表示调用方指明了后继节点有可能需要被唤醒,因为此方法是获取读锁
//过程调用,那么后面节点很可能也要获取读锁
//2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的后继节点是共享类型获取没有后继节点,则进行唤醒
//这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
//这里的初衷是后一个节点正好是共享节点,就唤醒,实现共享,独占有锁释放时候唤醒
if (s == null || s.isShared())
//后面详细说
doReleaseShared();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
}
注:这个唤醒操作doReleaseShared()在 releaseShared()方法里也会调用。唤醒后面想获取锁的节点。
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private void doReleaseShared() {
for (;;) {
//唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
//其实就是唤醒上面新获取到共享锁的节点的后继节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示后继节点需要被唤醒
if (ws == Node.SIGNAL) {
//这里需要控制并发,因为入口有setHeadAndPropagate跟releaseShared两个
//,避免两次unpark
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//执行唤醒操作
unparkSuccessor(h);
}
//如果后继节点暂时不需要唤醒,则把当前节点状态设置为
//PROPAGATE(=-3共享锁模式下需要进行传播唤醒后继共享节点)确保以后可以传递下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果头结点没有发生变化,表示设置完成,退出循环
//如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,
//必须进行重试
if (h == head)
break;
}
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱节点状态为signal,返回true
return true;
// 前驱节点状态为CANCELLED
if (ws > 0) {
// 从队尾向前寻找第一个状态不为CANCELLED的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱节点的状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
}
这里的逻辑也非常的绕,当多个线程同时调用addWaiter添加到队列中后,并且假设这些节点的第一个节点的前一个节点就是head节点,那么第一个节点就能加锁成功(假设都是SHARED节点),其余的节点在第一个节点设置头节点之前都会进入shouldParkAfterFailedAcquire方法,这时候waitStatus都等于0,所以继续自旋不会park,若再次加锁还失败就会park(因为这时候waitStatus=-1),但都是读线程的情况下一般都不会出现,因为setHeadAndPropagate第一步就是修改head,所以其余SHARED节点最终都能加锁成功并一直将唤醒传播下去。
锁释放
/**
*读锁
*/
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
public void unlock() {
sync.releaseShared(1);
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//同上读锁分析
doReleaseShared();
return true;
}
return false;
}
}
释放锁tryReleaseShared由子类Sync实现
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 清理firstReader缓存 或 readHolds里的重入计数
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
//非重入直接释放锁
firstReader = null;
else
//重入则减一
firstReaderHoldCount--;
} else {
//获取最后一次加读锁的重入次数记录器HoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
// 完全释放读锁
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count; // 主要用于重入退出
}
// 循环在CAS更新状态值,主要是把读锁数量减 1
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 释放读锁对其他读线程没有任何影响,
// 但可以允许等待的写线程继续,如果读锁、写锁都空闲。
return nextc == 0;
}
}
}
/**
* 写锁
*/
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
public void unlock() {
sync.release(1);
}
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
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由子类Sync实现
abstract static class Sync extends AbstractQueuedSynchronizer {
/** 获取写锁数量 将state的2进制低16位并上16个1(2的16次方减1) */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
//如果持有当前锁的线程不是自己,就报错
throw new IllegalMonitorStateException();
// 计算释放后state值,当前state-1
int nextc = getState() - releases;
//判断写锁数量是否等于0
boolean free = exclusiveCount(nextc) == 0;
if (free)
// 清空独占线程
setExclusiveOwnerThread(null);
// 写锁数量不为0,更新state值,
setState(nextc);
return free;
}
protected final boolean isHeldExclusively() {
//判断持有当前锁的线程是不是自己
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
小结
读写锁将state分为了高二字节和低二字节,分别存储读锁和写锁的状态,实现更为的复杂,在使用上还有几点需要注意:
-
读读共享,但是在读中间穿插了写的话,后面的读都会被阻塞,直到前面的写释放锁后,后面的读才会共享,相关原理看完前文不难理解。
-
读写锁只支持降级重入,不支持升级重入。因为如果支持升级重入的话,是会出现死锁的。如下面这段代码:
private static void rw() {
r.lock();
try {
log.info("获取到读锁");
w.lock();
try {
log.info("获取到写锁");
} finally {
w.unlock();
}
} finally {
r.unlock();
}
}
多个线程访问都能获取到读锁,但读写互斥,彼此都要等待对方的读锁释放才能获取到写锁,这就造成了死锁。
ReentrantReadWriteLock在某些场景下性能上不算高,因此Doug Lea在JDK1.8的时候又提供了一把高性能的读写锁StampedLock,前者读写锁都是悲观锁,而后者提供了新的模式——乐观锁,但它不是基于AQS实现的,下文会详解
Condition
Lock接口中还有一个方法newCondition,这个方法就是创建一个条件队列:
ReentrantLock的内部类Sync
public class ReentrantLock implements Lock, java.io.Serializable{
abstract static class Sync extends AbstractQueuedSynchronizer {
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
}
}
ConditionObject是AQS类的内部类
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
/** 队列的头结点 */
private transient Node firstWaiter;
/** 队列的尾结点 */
private transient Node lastWaiter;
public ConditionObject() { }
}
}
condition是要和lock配合使用的,而lock的实现原理又依赖于AQS,所以AQS内部实现了ConditionObject。我们知道在锁机制的实现上,AQS内部维护了一个双向的同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列。condition内部也是使用相似的方式,内部维护了一个单向的 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。
ConditionObject中有两个成员变量:头节点firstWaiter 和 尾节点lastWaiter ,同步队列的成员Node 复用了实现同步队列的内部类Node。用nextWaiter保存了下一个等待节点,结构如图
用Object的方式Object对象监视器上只能拥有一个同步队列和一个等待队列,而使用Lock可以有有一个同步队列和多个等待队列。可以多次调用lock.newCondition()创建多个Condition,所以一个Lock可以持有多个等待队列,如图。
await原理
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
public final void await() throws InterruptedException {
//当前线程如果中断,则抛异常
if (Thread.interrupted())
throw new InterruptedException();
// 加入条件队列
Node node = addConditionWaiter();
//因为Condition.await必须配合Lock.lock使用,所以await时就是将已获得锁的线程
//全部释放掉,并且唤醒在同步队列中头结点的后继节点引用的线程
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断是在同步队列还是条件队列,后者则直接park
while (!isOnSyncQueue(node)) {
//当前线程进入到等待状态
LockSupport.park(this);
// 获取打断处理方式(抛出异常或重设标记,中断,signal(),signalAll())
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 调用aqs的方法,自旋等待获取到同步状态(即获取到lock)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
// 清除掉已经进入同步队列的节点
unlinkCancelledWaiters();
// 处理被中断的情况
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
}
}
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
/**
* 加入条件队列,加入队列尾部
**/
private Node addConditionWaiter() {
Node t = lastWaiter;
// 清除状态为取消的节点
if (t != null && t.waitStatus != Node.CONDITION) {
//解除关联
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建一个CONDITION状态的节点并添加到队列末尾
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
}
}
将当前节点保存到新建立的Node,如果等待队列的firstWaiter为null的话(等待队列为空队列),则将firstWaiter指向当前的Node,否则,更新lastWaiter(尾节点)即可。可以看出等待队列是一个不带头结点的链式队列,而AQS中的同步队列是一个带头结点的链式队列。
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 使当前线程释放lock,并且唤醒在同步队列中头结点的后继节点引用的线程
**/
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;
}
}
public final boolean release(int arg) {
//先尝试释放锁
if (tryRelease(arg)) {
Node h = head;
//查看头结点的状态是否为SIGNAL
if (h != null && h.waitStatus != 0)
//是则唤醒头结点的下个节点关联的线程
unparkSuccessor(h);
return true;
}
return false;
}
}
方法内部调用AQS的模板方法release方法释放AQS的同步状态,并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。
while (!isOnSyncQueue(node)) {
// 3. 当前线程进入到等待状态
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,有两种可能:
-
逻辑走到break退出while循环(当前等待的线程被中断)
-
while循环中的逻辑判断为false(当前节点被移动到了同步队列中,即另外线程调用的condition的signal或者signalAll方法)。
总的说就是当前线程被中断或者调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后 ,这是当前线程退出await方法的前提条件。当退出while循环后就会调用acquireQueued(node, savedState)(之前Reentlock中讲过),自旋过程中线程不断尝试获取同步状态,直至获取lock成功。这也说明了退出await方法必须是已经获得了condition关联的lock。
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 判断是在同步队列还是条件队列
**/
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
/**
* 如果节点在同步队列中(从尾向后搜索),则返回true。
* 仅在isOnSyncQueue需要时调用。
* @return true(如果存在)
*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
}
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 已经入队的线程尝试获取锁
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //标记是否成功获取锁
try {
boolean interrupted = false; //标记线程是否被中断过
for (;;) {
final Node p = node.predecessor(); //获取前驱节点
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功,将当前节点设置为head节点
p.next = null; // 原head节点出队,在某个时间点被GC回收
failed = false; //获取成功
return interrupted; //返回是否被中断过
}
// 判断获取失败后是否可以挂起,若可以则挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程若被中断,设置interrupted为true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
signal原理
调用condition的signal唤醒一个等待在condition上的线程(头节点),将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回,源码如下。
AQS的内部类ConditionObject
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
public final void signal() {
//1. 先检测当前线程是否已经获取lock
if (!isHeldExclusively())
//没有获取lock,跑异常
throw new IllegalMonitorStateException();
//2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
}
}
signal方法首先会检测当前线程是否已经获取lock,没有获取lock会直接抛出异常,再调用doSignal传入头节点。doSignal方法源码为:
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
//头结点为空,尾结点也置为空
lastWaiter = null;
//1.头结点不为空,将头结点从等待队列中移除
first.nextWaiter = null;
//2. while中transferForSignal方法对头结点做真正的处理
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
}
//真正对头节点做处理的逻辑在transferForSignal放
final boolean transferForSignal(Node node) {
//1. 更新状态为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//2.将该节点移入到同步队列中去
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
}
关键逻辑请看注释,这段代码主要做了两件事情。
-
将头结点的状态更改为CONDITION
-
调用enq方法,将该节点尾插入到同步队列中。
由此可以看出,调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。
signalAll与signal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,代码如下。
/**
* AQS
* @since 1.5
* @author Doug Lea
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
public final void signalAll() {
if (!isHeldExclusively())
//如果当前线程没有持有锁,跑异常
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
//循环将条件队列中的所有节点移到同步队列中
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
}
}
该方法将等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。
其它组件
上文分析的锁都是用来实现并发安全控制的,而对于多线程协作JUC又基于AQS提供了CountDownLatch、CyclicBarrier、Semaphore等组件,下面一一分析。
CountDownLatch
CountDownLatch在创建的时候就需要指定一个计数:
CountDownLatch countDownLatch = new CountDownLatch(5);
然后在需要等待的地方调用**countDownLatch.await()方法,然后在其它线程完成任务后调用countDownLatch.countDown()**方法,每调用一次该计数就会减一,直到计数为0时,await的地方就会自动唤醒,继续后面的工作,所以CountDownLatch适用于一个线程等待多个线程的场景
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
//内部类
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
}
与前面讲的锁一样,也有一个内部类Sync继承自AQS,并且在构造时就将传入的计数设置到了state属性,看到这里不难猜到CountDownLatch的实现原理了。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* AQS
* **/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//首先判断是否被中断,中断就抛出异常,
if (Thread.interrupted())
throw new InterruptedException();
//否则与tryAcquireShared(arg)的返回值相比较,具体实现如下
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
}
//CountDownLatch的内部类
private static final class Sync extends AbstractQueuedSynchronizer {
/**
* 首先明白state状态变量,state的值代表着待达到条件的线程数,比如初始化为5,
* 表示待达到条件的线程数为5,每次调用countDown()函数都会减一,所以当没有达到条件
* 也就是state不等于0将会返回-1,进入if语句
**/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
}
/**
* AQS
* **/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//用于将当前线程相关的节点加入链表尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {//将入无限for循环
final Node p = node.predecessor(); //获得它的前节点
if (p == head) {
//(getState() == 0) ? 1 : -1;
int r = tryAcquireShared(arg);
//唯一的退出条件,也就是await()方法返回的条件很重要!!
if (r >= 0) {//表示state!=0
//处理后继节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return; //到这里返回
}
}
//挂起等待被唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//线程由该函数来阻塞的的
throw new InterruptedException();
}
} finally {
if (failed)//如果失败或出现异常,失败 取消该节点,以便唤醒后续节点
cancelAcquire(node);
}
}
//两个入参,node是当前成功获取共享锁的节点,
//propagate就是tryAcquireShared方法(尝试加锁,成功返回1,失败返回-1,也有可能返回0)
//的返回值,注意上面说的,它可能大于0也可能等于0
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; //记录当前头节点
//设置新的头节点,即把当前获取到锁的节点设置为头节点
//注:这里是获取到锁之后的操作,不需要并发控制
setHead(node);
//这里意思有两种情况是需要执行唤醒操作
//1.propagate > 0 表示调用方指明了后继节点有可能需要被唤醒,因为此方法是获取读锁
//过程调用,那么后面节点很可能也要获取读锁
//2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的后继节点是共享类型获取没有后继节点,则进行唤醒
//这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
//这里的初衷是后一个节点正好是共享节点,就唤醒,实现共享,独占有锁释放时候唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
private void doReleaseShared() {
for (;;) {
//唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
//其实就是唤醒上面新获取到共享锁的节点的后继节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示后继节点需要被唤醒
if (ws == Node.SIGNAL) {
//这里需要控制并发,因为入口有setHeadAndPropagate跟releaseShared两个
//,避免两次unpark,将头结点状态置为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//执行唤醒后继操作
unparkSuccessor(h);
}
//如果后继节点暂时不需要唤醒,则把当前节点状态设置为
//PROPAGATE=-3(共享锁模式下需要进行传播唤醒后继共享节点)确保以后可以传递下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果头结点没有发生变化,表示设置完成,退出循环
//如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,
//必须进行重试
if (h == head)
break;
}
}
}
在await方法中使用的是可打断的方式获取的共享锁,同样除了tryAcquireShared方法,其余的都是复用的之前分析过的代码,而tryAcquireShared就是判断state是否等于0,不等于就阻塞。
public void countDown() {
sync.releaseShared(1);
}
/**
* AQS
* **/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//唤醒后继等待的节点
doReleaseShared();
return true;
}
return false;
}
}
//CountDownLatch的内部类
private static final class Sync extends AbstractQueuedSynchronizer {
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
而调用countDown就更简单了,每次对state递减,直到为0时才会调用doReleaseShared释放阻塞的线程。最后需要注意的是CountDownLatch的计数是不支持重置的,每次使用都要新建一个。
CountDownLatch总结
CountDownLatch还提供了超时等待机制,在特定时间后就不会再阻塞当前线程;不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。一个线程调用countDown方法happen-before另外一个线程调用await方法。
CountDownLatch底层实现依赖于AQS共享锁的实现机制,首先初始化计数器count,调用countDown()方法时,计数器count减1,当计数器count等于0时,会唤醒AQS等待队列中的线程。调用await()方法,线程会被挂起,它会等待直到count值为0才继续执行,否则会加入到等待队列中,等待被唤醒。
CyclicBarrier
CyclicBarrier和CountDownLatch使用差不多,不过它只有await方法。CyclicBarrier在创建时同样需要指定一个计数,当调用await的次数达到计数时,所有线程就会同时唤醒,相当于设置了一个“起跑线”,需要等所有运动员都到达这个“起跑线”后才能一起开跑。
另外它还支持重置计数,提供了reset方法。
CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。
看如下示意图,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。
//同步操作锁
private final ReentrantLock lock = new ReentrantLock();
//线程拦截器
private final Condition trip = lock.newCondition();
//每次拦截的线程数
private final int parties;
//换代前执行的任务
private final Runnable barrierCommand;
//表示栅栏的当前代
private Generation generation = new Generation();
//计数器
private int count;
//静态内部类Generation
private static class Generation {
boolean broken = false;
}
上面贴出了CyclicBarrier所有的成员变量,可以看到CyclicBarrier内部是通过条件队列trip来对线程进行阻塞的,并且其内部维护了两个int型的变量parties和count,parties表示每次拦截的线程数,该值在构造时进行赋值。count是内部计数器,它的初始值和parties相同,以后随着每次await方法的调用而减1,直到减为0就将所有线程唤醒。CyclicBarrier有一个静态内部类Generation,该类的对象代表栅栏的当前代,就像玩游戏时代表的本局游戏,利用它可以实现循环等待。barrierCommand表示换代前执行的任务,当count减为0时表示本局游戏结束,需要转到下一局。在转到下一局游戏之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定barrierCommand来执行自己的任务。我用一图来描绘下 CyclicBarrier 里面的一些概念:
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
CyclicBarrier提供了两个构造方法,我们可以传入一个Runnable类型的回调函数,当达到计数时,由最后一个调用await的线程触发执行。其中构造器1是它的核心构造器,在这里你可以指定本局游戏的参与者数量(要拦截的线程数)以及本局结束时要执行的任务,还可以看到计数器count的初始值被设置为parties。CyclicBarrier类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的方法,分别是定时等待和非定时等待。
//非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe);
}
}
//定时等待
public int await(long timeout, TimeUnit unit) throws InterruptedException,
BrokenBarrierException, TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
//核心等待方法
private int dowait(boolean timed, long nanos) throws InterruptedException,
BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
//检查当前栅栏是否被打翻
if (g.broken) {
throw new BrokenBarrierException();
}
//检查当前线程是否被中断
if (Thread.interrupted()) {
//如果当前线程被中断会做以下三件事
//1.打翻当前栅栏
//2.唤醒拦截的所有线程
//3.抛出中断异常
breakBarrier();
throw new InterruptedException();
}
//每次都将计数器的值减1
int index = --count;
//计数器的值减为0则需唤醒所有线程并转换到下一代
if (index == 0) {
boolean ranAction = false;
try {
//唤醒所有线程前先执行指定的任务
final Runnable command = barrierCommand;
if (command != null) {
command.run();
}
ranAction = true;
//唤醒所有线程并转到下一代
nextGeneration();
return 0;
} finally {
//确保在任务未成功执行时能将所有线程唤醒
if (!ranAction) {
breakBarrier();
}
}
}
//如果计数器不为0则执行此循环
for (;;) {
try {
//根据传入的参数来决定是定时等待还是非定时等待
if (!timed) {
trip.await();
}else if (nanos > 0L) {
nanos = trip.awaitNanos(nanos);
}
} catch (InterruptedException ie) {
//若当前线程在等待期间被中断则打翻栅栏唤醒其他线程
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
//若在捕获中断异常前已经完成在栅栏上的等待, 则直接调用中断操作
Thread.currentThread().interrupt();
}
}
//如果线程因为打翻栅栏操作而被唤醒则抛出异常
if (g.broken) {
throw new BrokenBarrierException();
}
//如果线程因为换代操作而被唤醒则返回计数器的值
if (g != generation) {
return index;
}
//如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
上面贴出的代码中注释都比较详细,我们只挑一些重要的来讲。可以看到在dowait方法中每次都将count减1,减完后立马进行判断看看是否等于0,如果等于0的话就会先去执行之前指定好的任务,执行完之后再调用nextGeneration方法将栅栏转到下一代,在该方法中会将所有线程唤醒,将进入计数器的值重新设为parties,最后会重新设置栅栏代次,在执行完nextGeneration方法之后就意味着游戏进入下一局。如果计数器此时还不等于0的话就进入for循环,根据参数来决定是调用trip.awaitNanos(nanos)还是trip.await()方法,这两方法对应着定时和非定时等待。如果在等待过程中当前线程被中断就会执行breakBarrier方法,该方法叫做打破栅栏,意味着游戏在中途被掐断,设置generation的broken状态为true并唤醒所有线程。同时这也说明在等待过程中有一个线程被中断整盘游戏就结束,所有之前被阻塞的线程都会被唤醒。线程醒来后会执行下面三个判断,
- 看看是否因为调用breakBarrier方法而被唤醒,如果是则抛出异常;
- 看看是否是正常的换代操作而被唤醒,如果是则返回计数器的值;
- 看看是否因为超时而被唤醒,如果是的话就调用breakBarrier打破栅栏并抛出异常。
这里还需要注意的是,如果其中有一个线程因为等待超时而退出,那么整盘游戏也会结束,其他线程都会被唤醒。下面贴出nextGeneration方法和breakBarrier方法的具体代码。
//切换栅栏到下一代
private void nextGeneration() {
//唤醒条件队列所有线程
trip.signalAll();
//设置计数器的值为需要拦截的线程数
count = parties;
//重新设置栅栏代次
generation = new Generation();
}
//打翻当前栅栏
private void breakBarrier() {
//将当前栅栏状态设置为打翻
generation.broken = true;
//设置计数器的值为需要拦截的线程数
count = parties;
//唤醒所有线程
trip.signalAll();
}
最后,我们来看看怎么重置一个栅栏:
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
我们设想一下,如果初始化时,指定了线程 parties = 4,前面有 3 个线程调用了 await 等待,在第 4 个线程调用 await 之前,我们调用 reset 方法,那么会发生什么?
首先,打破栅栏,那意味着所有等待的线程(3个等待的线程)会唤醒,await 方法会通过抛出 BrokenBarrierException 异常返回。然后开启新的一代,重置了 count 和 generation,相当于一切归零了。
至此我们难免会将CyclicBarrier与CountDownLatch进行一番比较。
相同点:这两个类都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。
区别:
- 是CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
- CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能,例如上面的赛马程序就只能使用CyclicBarrier来实现。总之,这两个类的异同点大致如此,至于何时使用CyclicBarrier,何时使用CountDownLatch,还需要读者自己去拿捏。
- CyclicBarrier还提供了:resert()、getNumberWaiting()、isBroken()等比较有用的方法。
Semaphore
Semaphore是信号的意思,或者说许可,可以用来控制最大并发量。初始定义好有几个信号,然后在需要获取信号的地方调用acquire方法,执行完成后,需要调用release方法回收信号。
1.类的继承关系
public class Semaphore implements java.io.Serializable {}
说明:Semaphore实现了Serializable接口,即可以进行序列化。
2.类的内部类
Semaphore总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。
说明:Semaphore与ReentrantLock的内部类的结构相同,类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。
(1)Semaphore内部类Sync
Sync类的源码如下。
// Semaphore内部类,继承自AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
// 版本号
private static final long serialVersionUID = 1192457210091910933L;
// 构造函数
Sync(int permits) {
// 设置状态数
setState(permits);
}
// 获取许可
final int getPermits() {
return getState();
}
// 共享模式下非公平策略获取
final int nonfairTryAcquireShared(int acquires) {
for (;;) { // 无限循环
// 获取许可数
int available = getState();
// 剩余的许可
int remaining = available - acquires;
if (remaining < 0 ||
// 许可小于0或者比较并且设置状态成功
compareAndSetState(available, remaining))
return remaining;
}
}
// 共享模式下进行释放
protected final boolean tryReleaseShared(int releases) {
for (;;) { // 无限循环
// 获取许可
int current = getState();
// 可用的许可
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next)) // 比较并进行设置成功
return true;
}
}
// 根据指定的缩减量减小可用许可的数目
final void reducePermits(int reductions) {
for (;;) { // 无限循环
// 获取许可
int current = getState();
// 可用的许可
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next)) // 比较并进行设置成功
return;
}
}
// 获取并返回立即可用的所有许可
final int drainPermits() {
for (;;) { // 无限循环
// 获取许可
int current = getState();
// 许可为0或者比较并设置成功
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
}
说明:Sync类的属性相对简单,只有一个版本号,Sync类存在如下方法和作用如下。
(2)Semaphore内部类NonfairSync
NonfairSync类继承了Sync类,表示采用非公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法,其源码如下。
static final class NonfairSync extends Sync {
// 版本号
private static final long serialVersionUID = -2694183684443567898L;
// 构造函数
NonfairSync(int permits) {
super(permits);
}
// 共享模式下获取
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
//Semaphore
abstract static class Sync extends AbstractQueuedSynchronizer {
// 共享模式下非公平策略获取
final int nonfairTryAcquireShared(int acquires) {
for (;;) { // 无限循环
// 获取许可数
int available = getState();
// 剩余的许可
int remaining = available - acquires;
if (remaining < 0 ||
// 许可小于0或者比较并且设置状态成功
compareAndSetState(available, remaining))
return remaining;
}
}
}
说明:从tryAcquireShared方法的源码可知,其会调用父类Sync的nonfairTryAcquireShared方法,表示按照非公平策略进行资源的获取。
(3)Semaphore内部类FairSync
FairSync类继承了Sync类,表示采用公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法,其源码如下。
/**
* Fair version
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) { // 无限循环
if (hasQueuedPredecessors()) // 同步队列中存在其他节点
return -1;
// 获取许可
int available = getState();
// 剩余的许可
int remaining = available - acquires;
if (remaining < 0 ||
// 剩余的许可小于0或者比较设置成功
compareAndSetState(available, remaining))
return remaining;
}
}
}
说明:从tryAcquireShared方法的源码可知,它使用公平策略来获取资源,它会判断同步队列中是否存在其他的等待节点。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
而permits就是state的值。
acquire函数,此方法从信号量获取一个(多个)许可,在提供一个许可前一直将线程阻塞,或者线程被中断,其源码如下
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* AQS
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
}
//公平非公平都调用重新的tryAcquireShared,详解见上
说明:该方法中将会调用Sync对象的acquireSharedInterruptibly(从AQS继承而来的方法)方法,而acquireSharedInterruptibly方法在上一篇CountDownLatch中已经进行了分析,在此不再累赘。
最终可以获取大致的方法调用序列(假设使用非公平策略)。如下图所示。
release函数,此方法释放一个(多个)许可,将其返回给信号量,源码如下。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
说明:该方法中将会调用Sync对象的releaseShared(从AQS继承而来的方法)方法,而releaseShared方法在上一篇CountDownLatch中已经进行了分析,在此不再累赘。
最终可以获取大致的方法调用序列(假设使用非公平策略)。如下图所示。
Semaphore 的acquire方法和CountDownLatch是一样的,只是tryAcquireShared区分了公平和非公平方式。获取到信号相当于加共享锁成功,否则则进入队列阻塞等待;而Semaphore 的release方法和读锁解锁方式也是一样的,只是每次release都会将state+1。
总结
本文详细分析了AQS的核心原理、锁的实现以及常用的相关组件,掌握其原理能让我们准确的使用JUC下面的锁以及线程协作组件。
另外AQS代码设计是非常精良的,有非常多的细节,精简的代码中把所有的情况都考虑到了,细细体味对我们自身编码能力也会有很大的提高。
参考文档:
https://blog.youkuaiyun.com/fuyuwei2015/article/details/83719444
https://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&mid=2247509723&idx=2&sn=ee2c8b01fce7d97b9f2742532da681bc&chksm=eb5023eddc27aafb1c3e89da7926004096e609c08e06905445fdf343c8df108acd13e109a317&mpshare=1&scene=1&srcid=10125ItLFl64Q25OZQiZyDgB&sharer_sharetime=1602568699444&sharer_shareid=59b8231f277f480d075171da124a1b71&version=3.0.31.2998&platform=win&rd2werd=1#wechat_redirect
https://blog.youkuaiyun.com/zhaocuit/article/details/89604641
https://blog.youkuaiyun.com/sinat_32873711/article/details/106619981
https://blog.youkuaiyun.com/weixin_33856370/article/details/93177644
https://juejin.cn/post/6844903865154797581
https://blog.youkuaiyun.com/m0_37778687/article/details/96462383#三.%C2%A0%20StampedLock三种锁的转换
https://blog.youkuaiyun.com/qq_39241239/article/details/87030142
https://www.cnblogs.com/leesf456/p/5414778.html