花了一段时间学习了AbstractQueuedSynchronizer,研究了源码以及博客,这篇文章主要以自己的理解去一起学习AQS。
AQS简介
AQS是简写,全称是AbstractQueuedSynchronizer,在java.util.concurrent.locks
包下面,这个类是Java并发中的一个核心类,从名字里面可以看出来,里面维护这一个双向队列(queue)。和synchronize不同,它在Java语言层面实现了锁,而synchronize则是借用了JVM以及操作系统的。所以会说Reentrant更细腻。
与ReentrantLock的关系
前面说过,AQS里面维护着一个队列,那么队列里面又是什么呢?
主要是线程,以及其他属性。
相信很多人都用过Java里面的锁,除了可以用Synchronize
,同样可以用ReentrantLock
来实现,ReentrantLock就是用AQS来实现的,它继承自AQS,AQS更像一个框架,如果你想实现锁,共享锁或者排他锁,你需要遵循他的规范去继承它,并且重写方法就可以实现,这里可以看我分析ReentrantLock这篇文章。
AQS队列结构
前面说过了,AQS里面维护这一个queue,它是双向的,按照类里面的含义,这条队列是先入先出的,即FIFO。那么里面的Node节点里面又是咋样的呢?
static final class Node {
static final Node SHARED = new Node(); /** 默认是共享模式 */
static final Node EXCLUSIVE = null; /** 排他模式 */
static final int CANCELLED = 1; /** 表示当前线程被取消 */
static final int SIGNAL = -1; //表示当前节点要被进入sync时,的后继节点要被唤醒,也就是unparking
static final int CONDITION = -2; // 表示当前节点在等待condition,也就是在condition队列中。
static final int PROPAGATE = -3; // 表示当前节点的后续的,acquireShared 能够被执行。
volatile int waitStatus; //初始为0 当为0的时候,就处于sync队列中,等待着获取锁。
volatile Node prev; //在检查waitStatus情况下,的前一个节点。
volatile Node next; //后一个节点,当被出队时就会被gc。
volatile Thread thread; //当前执行的线程。
Node nextWaiter; //在condition队列的下一个等待着,或者是share模式的下一个。
}
如上所示,Node节点里面有5个状态:
- CANCELLED
- SIGNAL
- CONDITION
- PROPAGATE
- 0
具体的解释在上面代码中已经有体现。
那么这个Node,什么时候会被使用呢?
在用到锁的时候,我们有个基础的理解,如果一个线程获取不到锁,它会怎么办,它会等待继续尝试获取,或者进入挂起进入等待队列(不消耗cpu)。所以应该知道了,这里的Node是用来存储线程的。下面我将通过AQS源码从以下几个方面分析AQS的结构
AQS结构分析
先看AQS的声明:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
}
继承自一个父类AbstractOwnableSynchronizer
而这个父类里面,只是很简单的定义了一个当前拥有排他锁的线程:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
protected AbstractOwnableSynchronizer() { }
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
AQS,负责管理线程状态, 子类主要通过对volatile变量state的操作去决定是否获取锁。然后再决定是否进入队列。
AQS里面的state
AQS里面有个state,它是volatile类型的,它是用来干嘛的呢?
关于volatile,可以看我这篇文章:Java并发学习(二)-JMM
首先思考下,为什么能够判定某一个线程能够获取锁呢?
答案就是这个state,在AQS里面有一些方法,专门用于去给子类重写的:
...
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
...
这里只是给了一个头,具体有子类去实现,接下来看看ReentrantLock的实现:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
上面只贴出了ReentrantLock里面的非公平锁的重写,首先调用父类的getState()获取state变量,在尝试用原子性CAS方式compareAndSetState
来尝试设置state的值,实际上也就是尝试获取锁。设置成功就返回true,失败的话再检查是否为同一个线程 如果同一个线程,则在一定范围下也获取成功。否则获取不成功。
关于非公平锁,可以看我的专门分析Reentrant的文章。
经过了上面的分析,大概可以清楚,AQS是如何控制资源,并且给管理线程获取状态的。
- volatile类型的state
- CAS原子性修改
AQS中锁的获取与释放
上一个小结中,主要通过Reentrant为例大致说明了state的作用,接下来看AQS里面获取锁的思想。
AQS给子类提供了两种类型的锁:
- 排他锁 (EXCLUSIVE)
- 共享锁 (SHARED)
排他锁
普通获取排他锁的过程:acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
arg为用户自定义子类传回的state变量,由短路与,可以知道,当(tryAcquire)获取失败时候,才会执行&&后面条件判断。
接下来看addWaiter
方法。
private Node addWaiter(Node mode) {
//由mode,新建node,排他
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;
}
}
//先尝试一次入队,如果失败,就用自旋方式入队。
enq(node);
return node;
}
/**
* 节点入队操作。
* 插入到队尾
* 返回插入时候的这个tail尾节点。
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { //如果尾节点为null,就把头结点设为尾节点。
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//否则就就把node插入到队列最后。
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
由上代码,当获取锁失败,就需要在AQS里面记录嘛,就是插入这条队列,首先通过addWaiter
尝试插入一次,如果失败,则需要以自旋方式进行插入,一定得插入。都是利用CAS方式替换tail。
可中断获取排他锁:doAcquireInterruptibly
/**
* 在排他锁模式下,可中断的获取锁。
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
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)
//取消获取,状态设为cancelled。
cancelAcquire(node);
}
}
同样是先通过自旋的方式获取锁,如果自旋多次,仍然没有获取,并且前置节点已经发生了变化,则需要的时候将线程阻塞。具体什么变化呢?
接下来看shouldParkAfterFailedAcquire
。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*这个节点已经成功设置了状态,请求获取锁,所以只需要等
待前屈节点运行玩就会unpark自己,所以可以进行park。
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* 跳过被cancell的。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* condition或者 在sync,propagate
* 尝试设为signal。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
然后进行park操作:
private final boolean parkAndCheckInterrupt() {
//调用native方法进行park
LockSupport.park(this);
return Thread.interrupted();
}
如果获取失败,则需要cancell,看cancelAcquire
,代码比较复杂,看慢慢分析:
/**
* 当获取不到就会取消
*一旦发生异常,导致获取锁失败,则会调用cancelAcquire()方法"Cancels an ongoing attempt to acquire"。
* node是tail
* node是head
* node既不是tail,又不是head
* @param node the node
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
//1. node不再关联到任何线程
node.thread = null;
//2. 跳过被cancel的前继node,找到一个有效的前继节点pred
// 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;
//3. 将node的waitStatus置为CANCELLED
// 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;
//4. 如果node是tail,更新tail为pred,并使pred.next指向null
// 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;
//5. 如果node既不是tail,又不是head的后继节点
//则将node的前继节点的waitStatus置为SIGNAL
//并使node的前继节点指向node的后继节点(相当于将node从队列中删掉了)
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 {
//6. 如果node是head的后继节点,则直接唤醒node的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
上述cancelAcquire
一共有6步:
- node不再关联到任何线程
- 跳过被cancel的前继node,找到一个有效的前继节点pred
- 将node的waitStatus置为CANCELLED
- 如果node是tail,更新tail为pred,并使pred.next指向null
- 如果node既不是tail,又不是head的后继节点则将node的前继节点的waitStatus置为SIGNAL并使node的前继节点指向node的后继节点(相当于将node从队列中删掉了)
- 如果node是head的后继节点,则直接唤醒node的后继节点
超时获取排它锁doAcquireNanos:
超时获取排它锁中,主要就是添加了nanosTimeout
这个参数,用来判定是否已经过了设定时间,从而执行不同逻辑,其他的都与可中断排它锁一致,就不多说了。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
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;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
释放排它锁release
有获取锁当然也会有释放所,在ReentrantLock里面使用unlock来实现,这里主要讲当ReentrantLock调用到父类AQS代码时候操作:
/**
* 释放排他锁。
* 首先tryRelese释放标记
* 然后,排它锁获取的肯定是head出现,此时我只要唤醒(unpack)继任线程就可以了。
*/
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是由子类重写的方法,目的是为了改变state的值,从而代表释放资源。当释放成功后,unparkSuccessor(h)
,释放头结点的后继节点。
/**
* 如果继任者存在,唤醒继任者开始执行。
* 如果继任者waitStatus<0,则会将其设为=0进行
* 如果继任者>0(状态无效为cancelled。),则接着往后面找
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
//如果ws<0,则需要设为0,因为:值为0,表示当前节点在sync队列中,等待着获取锁。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//重新找一个successor.
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//找到了,解锁。
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor方法的作用就是唤醒继任节点,因为一般当前运行的节点会设为head,所以当要释放的时候,unpark后面这个节点就可以了。
当然需要过滤掉cancelled的节点。
共享锁
共享锁,就是指可以允许多个线程同时对某一资源进行访问,这里会有个最开始的问题,会不会有并发问题?因为AQS只是提供了一个并发的核心框架,具体是否有并发问题,要看子类的自我实现,以及子类工具的用途。
什么意思呢?
就好比说,exclusive模式下,AQS管理一次只能一个线程进行运行,运行完后按照规则传递给后继节点。
而share模式下,AQS管理的则是一次能让多个线程同时运行,可以设置同时进入的数量,运行完后再把排它锁状态往后延伸。
共享锁的普通获取acquireShared
首先看acquireShared方法:
/**
* 获取共享锁。
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* 获取共享锁。
* Acquires in shared uninterruptible mode.
* @param arg the acquire argument
*/
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//首先是这样去尝试获取共享锁。通过volatile变量。
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
和获取排他锁思路基本一致,都是先尝试获取,获取不到在进行park操作,但是还有两个地方不同:
- int r = tryAcquireShared(arg); if (r >= 0)
- setHeadAndPropagate(node, r);
第一个是通过子类实现的tryAcquireShared方法去判定是否能获取锁,还记得,最开始acquireShared里面也是执行了这个方法,所以这里是被执行了2次的,最后判断是否能够获取锁是通过r>=0
来判断的,所以是个范围,也就印证了这是个共享的锁。
第二个的setHeadAndPropagate,和排它锁(only setHead)不同,主要是为了设置head,以及把共享状态往后穿,大家应该还记得Node节点的waitState属性里面有个Propagate的值-3吧。
/**
* 设置头结点。
* tryAcquireShared执行相关。
* @param node the node
* @param propagate the return value from a tryAcquireShared
* 某个节点被设置为head之后,如果它的后继节点是SHARED状态的,那么将继续通过
* doReleaseShared方法尝试往后唤醒节点,实现了共享状态的向后传播。
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* 尽力去唤醒下一个节点。
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
doAcquireSharedInterruptibly以及doAcquireSharedNanos
超时获取以及可中断获取,在获取锁代码逻辑上与acquireShared基本一致,而在interruption以及超时判断上,则与排它锁一致,这里就不重复来说了,可以参看上面文章。
ConditionObject
还记得上面说过,在AQS的Node节点里面,有个waitState值为Condition值为-2.就是代表他在阻塞状态,类似于synchronize里面的wait,而ConditionObject也就可以充当Lock的wait和signal。后续会介绍Condition类具体分析。
先看ConditionObject里面内容:
public class ConditionObject implements Condition, java.io.Serializable {
...
/** 引用的头节点。 */
private transient Node firstWaiter;
/** 引用的尾节点。. */
private transient Node lastWaiter;
...
}
如上,在ConditionObject里面包含了两个重要的字段,都是Node类型,firstWaiter和lastWater;其实ConditionObject和AQS里面那条队列并不是同一条,由代码可以分析出来,请看我下文分析。
当然,里面方法主要就是await和signal。
- private Node addConditionWaiter(),添加当前线程作为waiter
- private void doSignal(Node first),唤醒firstWaiter
- private void doSignalAll(Node first),唤醒,所有waiter
- private void unlinkCancelledWaiters(),删除cancell的节点
- public final void signal(),对外唤醒任意一个,主要第一个
- public final void signalAll(),对外唤醒所有
- public final void awaitUninterruptibly(),不会中断的等待
- private int checkInterruptWhileWaiting(Node node),判断等待过程中是否发生了异常
- private void reportInterruptAfterWait(int interruptMode),等待后重新报告异常
- public final void await() throws InterruptedException ,对外的等待方法
- final boolean isOwnedBy(AbstractQueuedSynchronizer sync) ,判断是否为当前线程condition
- protected final boolean hasWaiters(),是否有waiter。
ConditionObject的主要就是用来阻塞队列以及唤醒队列的,而阻塞队列,一方面是把waitState改为condition,另一方面还需要执行park的native方法,而对于唤醒队列,则是把waitState改为0,执行unpark方法。思路理清楚了,接下来分析两个具体代码:
await方法
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
//首先释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//如果不在syncqueue里面,即waitState != 0,阻塞本线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
//中断了就直接退出
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
//清楚cancelled的节点
unlinkCancelledWaiters();
if (interruptMode != 0)
//判断,是纪录异常还是抛出
reportInterruptAfterWait(interruptMode);
}
如上代码,
1. 首先会addConditionWaiter方法,主要作用就是把本线程添加到condition队列的尾端,注意是根据lastWaiter来判定的,所以添加到的是condition队列,waitState为condition。
2. 其次,fullyRelease来释放当前线程锁,也就是占有的资源。
3. 下一步,利用自旋的方式isOnSyncQueue,判断是否仍然处于等待获取资源状态。如果是的就阻塞本线程,有错误就退出。
4. 最后判断是否有错误,以及是否被设置为抛出错误,ConditionObject里面用THROW_IE
和REINTERRUPT
来判断。
/**
* 重新报告interrupt类型。
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
signal方法
ConditionObject里面有两个signal,一个是有FIFO,唤醒头结点,另一个是signalAll,唤醒所有节点。
接下来看signal方法:
/**
* 默认唤醒第一个节点线程。
*/
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
如上里面会调用doSignal(first):
/**
* 唤醒一个waiter。
* 放入sync队列。
* @param first (non-null) the first node on condition queue
*/
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
自旋阻塞式的,直到成功被唤醒,具体细节在transferForSignal(first)
方法里面:
/**
* 唤醒节点,状态改为0。
* 唤醒成功的话,就插入队尾并且如果此时队尾元素cancell或者强行设置队尾元素失败,那么就需要唤醒此时队尾元素。
*/
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
* 设置成功了!
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 把前一个节点设为signnal。
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
//放到最后重新排队。
Node p = enq(node);
int ws = p.waitStatus;
//检查前一个节点是否为signal。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
transferForSignal方法的目的就是讲节点waitState值设为0,并且添加到AQS队列的尾端。
收获
看了多遍源码即博客,才慢慢理解了AQS运行的原理,Doug Lea大佬真是厉害,从AQS里面也读到了两个比较有特色的写法:
- 使用&& 和||短路与和短路或去实现if的操作,里面用的真多。
- 在一个条件判断中,我发现同样的条件用&&连接了两次,后来知道是为了检查两次二故意做的。因为在并发环境下,很可能多做一次,就得到想要的结果了。
/**
* 获取队列第一条线程。
* 有头结点就判断头结点,没有头结点,就从尾端一直找到头结点。
* Version of getFirstQueuedThread called when fastpath fails
*/
private Thread fullGetFirstQueuedThread() {
Node h, s;
Thread st;
//两段判断一模一样,检查了两次
if (((h = head) != null && (s = h.next) != null &&
s.prev == head && (st = s.thread) != null) ||
((h = head) != null && (s = h.next) != null &&
s.prev == head && (st = s.thread) != null))
return st;
//审查不通过,那就从尾端开始
Node t = tail;
Thread firstThread = null;
while (t != null && t != head) {
Thread tt = t.thread;
if (tt != null)
firstThread = tt;
t = t.prev;
}
return firstThread;
}
分析有诸多不足之处,如有错误,还请指出~
参考文章:
http://ifeve.com/introduce-abstractqueuedsynchronizer/
https://www.cnblogs.com/leesf456/p/5350186.html