前言
- 文章内容均以java 8_u151版本的代码为准。
- 文章内容绝大部分都是个人见解,如有错误的地方,欢迎大家指正。
概述
- AbstractQueuedSychronizer(AQS)是一个concurrent的framework,AQS从java5+支持,是一个抽象类,继承了AbstractOwnableSychronizer(后面统称为AOS),但AOS却是从java6才添加的,也是个抽象类。AOS类的作用很简单,源码中只包含一个成员变量和对应成员变量的get set方法,作用就是记录Thread对象,主要用于排它锁模式中,用于记录获取到AQS state(也就是锁资源)的线程对象。
- AQS有一个abstract subclass,AbstractQueuedLongSychronizer(AQLS),这个也是从java6才添加,与AQS的区别是,AQLS的state字段是long类型的,而AQS是int类型。
- AQS框架的内部维护着一个int类型的state变量,这个变量对于共享锁(semaphore为例)和排它锁(ReentrantLock为例)来说,具有不同的意义。对于共享锁,state表示共享许可证的数量(也可以参考令牌桶,对应令牌的数量),每个线程需要获取N(N<=state)个许可证才能运行,每次获取到许可后,state的数量都要相应减少,当线程所需的许可证数量大于state剩余数量时,就会进入阻塞状态;对于排它锁,state表示当前锁资源被分配的次数,初始值是0,当线程A每次执行lock()方法时,state++,当线程B执行lock()方法时,因为state非0,且state的所属者为ThreadA,就会进入阻塞模式。
- 对于AQS state的修改,都是通过AQS内部的线程安全方法实现。而这些方法都是final修饰的,因此不允许外部重写。
- 在使用AQS时,JDK官方要求要以helper class的方式提供服务,即AQS的实现类要是某个class的内部类,且不能用public修饰。
- AQS分为exclusive mode和share mode,当处于排它锁模式时,只能有一个线程能够acquire state;而当处于共享锁模式时,允许多个线程同时持有state(但也可以不允许多个线程持有)。
- AQS的序列化只保留了state的信息,而对于queue中的信息不会序列化,因此如果需要序列化,需要重写readObject方法。
-
要使用AQS,需要重写一些方法。share mode:tryAcquireShared(int);tryReleaseShared(int)。exclusive mode:tryAcquire(int);tryRelease(int);isHeldExclusively()。通常情况下,一个AQS的实现只会实现其中一个mode,因此不需要实现的mode锁需要的方法不需要重写。
Exclusive Mode解析
下面以ReentrantLock为例,讲解AQS exclusive mode的工作流程。
ReentrantLock分为公平锁(Fair Lock)和非公平锁(Non Fair Lock),通过构造方法来指定使用的锁类型。默认的构造函数创建的是Non Fair Lock。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
NonFair Lock
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
首先看一下非公平锁的AQS实现,使用non-pulic static inner class,继承自abstract Sync class,这个Sync继承了AbstractQueuedSychroinzer,且二者均使用无参构造函数。
因此,在创建ReentrantLock时,首先AQS的state对象值为0(这里与共享锁不一样,共享锁的state初始化时设置了一个给定的值,每次通过acquire获取到permit时,都会减一)。
当调用reentrantLock.lock()获取锁时,实际出发的是NonFairSync.lock()方法,Non Fair Sync的lock方法逻辑如上所示,使用CAS尝试将AQS.state由0修改为1,如果修改成功,就认为lock()执行成功,此时调用AOS(AbstarctOwnableSychronizer).setExclusiveOwnerThread方法,记录AQS.state的持有线程。如果CAS修改失败(被其他线程抢占锁资源),那么进入acquire(1)的逻辑。
acquire(1)方法触发的是AQS的内部final method,源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先注意:acquire是final修饰的,因此无法被继承重写。
在acquire方法中,首先执行tryAcquire(1)第二次尝试获取AQS.state资源,如果返回true,表示获取成功;如果返回false,则currentThread需要进入AQS的阻塞队列中(CLH队列)。
tryAcquire()方法在AQS内部并没有任何实现,因此属于需要开发人员重写的方法,该方法的重写实现在第一个代码片段中,其内部最终调用nonfairTryAcquire(1)方法,下面看一下这个方法的实现:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在第二次尝试获取锁资源的过程中,首先第一个if中的逻辑与lock()方法一样,要求AQS.state必须是0;第二个if判断属于锁的重入操作,从这里可以看出,ReentrantLock的重入,会通过AQS来记录锁重入的次数,但是同一个ReentrantLock的可重入次数只能是2^31-2次(第一次lock不认为是重入)。
现在假设,currentThread=Thread1,而目前AQS.state资源被Thread0占用,因此tryAcquire(1)方法返回false,会继续触发addWaiter()方法和acquireQueued()方法。
addWaiter()方法负责将Thread1封装在一个Node对象中,然后将Thread1 Node插入AQS FIFO queue队列的tail,源码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter的代码逻辑分为两块:AQS FIFO queue已经构建,此时只需要将Thread1 Node节点通过CAS的方式插入到queue tail即可;AQS FIFO queue没有构建,此时需要先初始化queue的head和tail节点,然后将Thread1 Node以CAS的方式插入queue。有关AQS FIFO queue的相关介绍,在文章最后。
在Thread1 enqueue到AQS的队列后,就会触发acquireQueued()方法进行第三次尝试获取AQS.state资源,源码如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued方法中,判断Node是否是FIFO队列中优先级最高的节点,如果优先级最高,尝试获取锁资源,获取成功就会解除Thread block并执行相应的业务代码;如果不满足优先级最高或者获取资源失败,则调用sholdParkAfterFailedAcquire()方法,这个方法的主要工作是清除node节点的canceled predecessor,并将node.pre的waitstatus设置成SIGNAL状态(对AQS FIFO queue中Node的状态解释,在文末部分)。
parkAndCheckInterrupt()方法的作用是通过LockSupport.park方法阻塞当前线程,并返回当前线程是否被interrupt。通过park阻塞后,当前线程只能被reentrantLock.release()或者其他线程通过interrupt唤醒,但无论任何唤醒方式,都需要进行自旋操作,第N次尝试获取AQS.state资源,直到能够成功获取时,才能正常解除阻塞并执行后续操作(例如因为interrupt造成抛出InterruptException异常)。
对于LockSupport的park和unpark,有两个注意点:
- park方法造成的block,当被外界interrup时,不会抛出InterruptException,只能通过Thread.isInterrupted来判断。
- JDK API中说到,park方法的block broken可以被三种情况触发,常见的两种方法是unpark和interrupt,还有一种是没有任何原因的自动broken,造成这个原因的相关文章可以参考https://blog.csdn.net/hengyunabc/article/details/28126139?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-4&spm=1001.2101.3001.4242;而对于无原因自动broken,官方给出的解决方案是自旋判断condition,在condition不满足时重新调用park方法。
分析到这一步,我们就可以猜测出整个ReentrantLock的lock与unlock机制了,lock阻塞后,unlock会从AQS FIFO queue中获取到queue header.next Node对于的Thread,通过调用LockSupport.unpark(thread)来唤醒,唤醒后的thread通过自旋重新进入tryAcquire()方法尝试获取AQS.state,获取成功后修改AQS FIFO queue的header节点。这里可以发现,Non Fair Lock不公平的原因是,从AQS FIFO queue中唤醒的thread1,在tryAcquire的时候,很可能被不存在于queue中的thread2抢占资源,从而导致thread1重新调用LockSupport.park进入阻塞。
因此,Fair Lock的实现就是在Non Fair Lock的基础上,在tryAcquire方法时,提前判断AQS FIFO queue中是否存在block的Node,如果存在,当前线程不应该直接获取锁,而是直接加入到queue tail中。
在acquireQueued方法中,还剩下最后一块finally语句块,要触发这部分,对于reentrantLock,只有当调用lock(long waitTime)或者lockInterrupt()这种允许线程在没有获取到AQS.state时也能自行进入runnable状态的方法,才会触发finally语句块中的cancelAcquire(node),这个方法的源码逻辑并不难,可以自行阅读,主要的作用就是设置node的waitstatus为CANCLE,并且当node是AQS FIFO queue队列中优先级最高的节点时,自动调用lock.unlcok的逻辑唤醒successor Node。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
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
}
}
AQS中的ConditionObject
AQS的ConditionObject是一个inner class,是ReentrantLock.newCondition核心对象。
这里只说明一点:condition.await方法时,Thread Node对象也是保存在FIFO队列,但不同于上面说到的AQS的FIFO queue,而是一个单独的condition FIFO queue,其数据结构类似于:
【Thread1 Node】->【Thread2 Node】
header Node一定是一个有效的Node(这不同于AQS FIFO queue,AQS queue在第一次初始化时的header Node是一个虚节点,用来表示当前reentrantLock.lock()成功获取到锁资源的虚拟对象),所有处于condition FIFO queue中的Node,其waitStatus一定是Condition。在其他线程通过condition.signal()唤醒Node时,Node从condition FIFO queue进入到AQS FIFO queue中,添加到队尾,然后继续按照AQS的逻辑执行资源抢占。
注意:无论是condition还是java sychronized原语中的Object.wait()方法,,在wait方法被外部interrupt后,必须获取到锁资源,才会抛出interruptException异常信息,否则即使被外部中断,只要没有资源,依然是死锁的。
Share Mode
AQS的共享模式,我们以Semaphore为例分析。
Semaphore相关介绍
- 从java5+支持。
- semaphore内部维护了N个permits(许可证),通过acquire(int n)方法尝试获取permit,如果获取不到就阻塞;通过release(int n)方法释放permit。注意:release操作是线程不敏感的,可以有ThreadA来释放ThreadB获取到的permit。
- Semaphore常用于对resource资源的控制,例如控制Pool中可用的连接数量。
- Semaphore分为公平锁和非公平锁,通过构造函数时指定:默认创建非公平锁,在非公平锁下,适合多线程任务的抢占式工作;对于公平锁,适合对于resource的获取,以防止线程饥饿;但需要注意:tryAcquire(int)方法会打破公平锁的功能,如果能够获取到permits,那么会直接获取,而不会加入到thread queue中,如果想使用公平锁的功能,可以使用tryAcquire(int,long=0,TimeUnit.SECONDS)方法来代替。
- Semaphore的permits设置为1时,就可以作为Lock对象使用,此时称为binary semaphore;这个用途可以用来防止死锁问题。
- Happen-Before原则:release()方法之前的代码一定早于acquire()之后的方法。
源码解析
Semaphore的await()方法会调用acquireSharedInterruptibly方法,可以看到,Share Mode所使用的API不同于Exclusive Mode,所有的方法都带有后缀Shared,例如tryAcquireShared()。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
在Share mode中,AQS.state的初始值等于permit的数量,每次调用tryAcquireShared方法,都有可能执行AQS.state--操作,对应的表示permitPool中的permit被某个thread获取。
当permitPool中的剩余数量无法满足Thread的需要时,就会进入Thread Node enqueue的逻辑,源码如下:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 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);
}
}
我们能够发现,share mode的enqueue几乎与exclusive mode一样,只有一点不同之处,setHeadAndPropagate()方法,可能同时唤醒N个queue node,而不仅仅是1个。
CLH Queue介绍
clh队列,全称Craig, Landin, and Hagersten queue,是自旋锁的主要数据结构,二AQS中所使用的FIFO queue,是CLH queue的一种变体,其结构如下:
queue需要一个head node,一个tail node和N各节点node,在出队列时,只需要将head节点设置成head.next就可以,需要注意的是,出队列一定是线程安全的,因此不需要任何原子性保证;入队列时,需要将新的node设置成tail,将old tail.next指向new node,入队列需要注意线程安全问题。
队列中每一个Node节点都会维护当前节点的status(0,SIGNAL(-1),CONDITION(-2),PROPAGATE(-3),CANCEL(1)),tail node status都是0,当enqueue发生后,old tail node的 status设置成SIGNAL,表示当前节点存在successor。
CONDITION状态只会出现在exclusive mode的ConditionObject中,用于表示当前Node是一个condition.await产生的节点。
PROPAGETE目前只有share mode中出现,但还弄明白出现的时机。
CANCEL状态表示当前NODE因为interrupt或者timeout取消获取,但是cancel node不会自己自动的从CLH队列中出队,只会被successor入队或者predecessor release资源的时候发现并剔除(如果使用lockInterrupt()方法,则可以通过抛出InterruptException来达到自动出队的效果)。