零零总总的学习了AQS的源码,在这里做一个汇总。
AQS全称AbstractQueuedSynchronizer,可以说是java并发包的基础,在它之上衍生了许多同步类,如ReentrantLock、CountDownLatch、Semaphore等。顾名思义,抽象的队列式的同步器,它定义了一套多线程访问共享资源的同步器框架。使用它的主要方式是继承。闲话少说,开始进入正题。
1、AQS概览
关键字:state、FIFO队列、两种资源共享方式
AQS使用一个volatile int变量state表示同步状态,使用内置的FIFO队列来完成资源获取线程的排队工作。
state
state的访问方式有三种,如下:
// 获取当前状态
protected final int getState() {
return state;
}
// 设置状态
protected final void setState(int newState) {
state = newState;
}
// cas设置状态
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
FIFO同步队列
它是一个链表实现的双端队列,先进先出,用来存放阻塞的线程。当前线程获取同步状态失败时,AQS会将当前线程和等待状态等信息构造成一个节点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;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
waitStatus包含如下状态:
- CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
- SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
- CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
- 0状态:值为0,代表初始化状态。
AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态。
出入队列相关的操作稍后再看。
两种资源共享方式
AQS定义两种资源共享的方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
2、如何使用AQS
要使用AQS,需要继承AQS并重写指定的方法,AQS在自定义的同步组件实现中,会调用这些由子类重写的方法。具体要重写的方法很简单,子类只需要实现共享资源的获取与释放的方式即可,而等待队列的维护,AQS中已经实现好了。子类需要重写的方法如下:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):以独占方式尝试获取资源,成功则返回true,失败则返回false。实现该方法需要查询当前同步状态并判断同步状态是否符合预期,然后在进行CAS设置同步状态
- tryRelease(int):以独占方式尝试释放资源,成功则返回true,失败则返回false。等待获取同步状态的线程有机会获取同步状态
- tryAcquireShared(int):以共享方式尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):以共享方式尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
为了方便使用,AQS本身对上面的方法提供了默认实现,具体为抛出UnsupportedOperationException,我们可以按需继承AQS实现相应的方法,不必全部实现。一般来说,继承AQS的自动以同步器要么是独占方式的,要么是共享方式的,只需要实现【独占获取+释放】、【共享获取+释放】中的一种即可。当然,AQS也支持独占共享两种方式都全部实现,例如ReentrantReadWriteLock。
继承AQS例子
独占锁
先看一个独占锁的示例。独占锁,就是在同一时刻只有一个线程能获取到锁,其他尝试获取锁的线程只能在同步队列中等待。只有获取锁的线程释放了锁,后续的线程才能释放锁。见代码:
public class Mutex implements Lock {
/**
* state 取值有两个,0,1
* 0表示没有线程独占,1表示有线程独占
*/
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (getState() != 0) {
return false;
}
/**
* state 为0且被cas设置为1,设置当前线程为独占线程
*/
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
/**
* 清除线程占用状态,设置状态为0
*/
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
Condition newCondition() {
return new ConditionObject();
}
}
private final Sync sync = new Sync();
/**
* 将锁操作代理到 sync上
*/
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
// 简单的测试代码
public class MutexTest {
private static final Mutex mutex = new Mutex();
public static void main(String[] args) {
for (int i = 0; i < 5; i ++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start run");
mutex.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
try {
Thread.sleep(1000);
} catch (Exception e) {
}
mutex.unlock();
System.out.println(Thread.currentThread().getName() + " unlock");
}
}, "thread " + i).start();
}
}
}
上面的独占锁,不能重入。所谓重入,就是已经获取锁的线程,再次尝试获取锁,能获取锁成功。下面看jdk自带的ReentrantLock可重入锁,是怎么实现的。
/**
* 可重入锁有公平锁与非公平锁,公平锁是指先排队的线程先获取锁,非公平状态下获取锁的顺序不受排队的限制。
* Sync是父类,NonfairSync和FairSync继承自Sync。
* state 使用含义,0表示锁未被获取。线程获取到锁后将state加1,再次重入后继续将state加1。释放锁会将state减1,减为0表示锁被释放
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 子类实现
abstract void lock();
// 非公平锁尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// c == 0表示锁未被占用,cas成功说明获取锁成功
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 当前线程已经获取了锁,重入,将state + 1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// state == 表示释放锁完成
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 尝试设置 state为1,成功表示加锁成功,失败则走正常流程。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 使用父类实现
return nonfairTryAcquire(acquires);
}
}
// 公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// 公平锁获取
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 跟非公平锁相比,多了 !hasQueuedPredecessors() 这个逻辑判断,即必须没有前置节点,才能获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
3、源码分析
独占式
需要用到AQS子类实现的两个方法:tryAcquire()和tryRelease(),这里对它们的返回值再说明下
- tryAcquire(int):以独占方式尝试获取资源,成功返回true,失败返回false。
- tryRelease(int):以独占方式尝试释放资源,成功返回true,失败返回false。
独占式获取锁-acquire
无论是上面自己实现的Mutex还是ReentrantLock,加锁lock()方法都是委托给了AQS的acquire方法。下面看acquire代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
代码不多,但是逻辑比较多,详细分析下。tryAcquire()方法就是需要子类实现的方法,tryAcquire()返回true表示加锁成功,方法执行完,直接退出。如果返回false,表示需要入等待队列,继续执行&&后面的逻辑。先执行addWaiter(Node.EXCLUSIVE), arg)这段逻辑,作用是往等待队列里面添加一个独占模式的节点,看代码
// 将当前线程加入到等待队列的队尾
private Node addWaiter(Node mode) {
//将当前线程封装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
// cas设置tail成功直接返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 尾节点为空或者cas失败,执行enq()逻辑
enq(node);
return node;
}
private Node enq(final Node node) {
// 循环的cas保证节点被成功添加
for (;;) {
Node t = tail;
// 队列为空,初始化头节点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter()会返回添加到队尾的节点,接下来看acquireQueued(final Node node, int arg)方法,其中主要逻辑为:每个节点进入等待队列后就开始自旋,即自我观察,直到满足条件获取到同步状态,就可以从自旋的过程中退出,否则就一直自旋。
final boolean acquireQueued(final AbstractQueuedSynchronizer.Node node, int arg) {
boolean failed = true; //是否成功获取资源
try {
boolean interrupted = false; //等待过程是否被中断
//自旋
for (;;) {
//前驱节点
final AbstractQueuedSynchronizer.Node p = node.predecessor();
//如果前驱节点是head,便可以尝试获取资源(可能是前驱节点释放资源唤醒当前节点,也有可能被interrupt)
if (p == head && tryAcquire(arg)) {
setHead(node); //设置头
p.next = null; // 将前面节点的next设置为null,帮助回收
failed = false;
return interrupted; //返回是否被中断过
}
//如果应该park(),就进入等待,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
先不着急结束acquireQueued()的分析,继续深入shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()。shouldParkAfterFailedAcquire()通过检查前置节点的状态来判断当前节点是否应该park
private static boolean shouldParkAfterFailedAcquire(AbstractQueuedSynchronizer.Node pred, AbstractQueuedSynchronizer.Node node) {
int ws = pred.waitStatus; //前驱节点的状态
if (ws == AbstractQueuedSynchronizer.Node.SIGNAL)
//如果已经告知前驱节点,在前驱节点释放锁或者取消后通知自己,则直接返回
return true;
if (ws > 0) {
//大于0表示节点无效,则一直往前找,找到正常等待的节点,排队在它的后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱节点正常,就把前驱的状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, AbstractQueuedSynchronizer.Node.SIGNAL);
}
return false;
}
如果线程可以park,则执行parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //调用park()使线程进入waiting状态
return Thread.interrupted(); //如果被唤醒,返回师傅被中断
}
LockSupport.park()会让当前线程进入waiting状态,在此状态下,有两种途径可以唤醒该线程,被unpark()和被interrupt()。
现在回到acquireQueued()的源码分析,整个过程就比较清晰了
- 尝试获取锁,成功则返回过程中是否被中断,失败则寻找到park点
- park进入waiting状态,等待被唤醒
- 被唤醒,继续流程1
acquireQueued()执行完成后,下面再回到acquire()方法,再贴一遍源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
下面对整个流程用流程图描述:
独占式释放锁-release
线程获取同步状态执行完逻辑后,需要释放同步状态,使得后续节点可以获取到同步状态,即AQS的release()方法,这个方法在是否同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态),来看代码:
public final boolean release(int arg) {
// 需要AQS子类实现的方法,释放同步状态后返回true
if (tryRelease(arg)) {
// 获取头结点
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒等待队列后继节点
unparkSuccessor(h);
return true;
}
return false;
}
逻辑比较简单,tryRelease()返回true表示已经释放掉同步状态,唤醒头结点的后继节点。需要注意,AQS子类实现tryRelease()时,当state == 0时,应该返回true,否则返回false。下面看unparkSuccessor()
private void unparkSuccessor(AbstractQueuedSynchronizer.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);
}
就是找到等待队列中排在最前面的有效的节点,unpark()唤醒它。这里可以跟上面的acquireQueued()联系起来,节点被唤醒后会尝试重新获取同步状态,获取到资源后返回。
共享式
需要用到AQS子类实现的两个方法:tryAcquireShared()和tryReleaseShared(),这里对它们的返回值再说明下:
- tryAcquireShared(int):以共享方式尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):以共享方式尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
共享式获取锁-acquireShared
共享式获取锁和独占式获取锁的最主要区别在于同一时刻能否有多个线程同时获取到同步状态。来看acquireShared()的源码
public final void acquireShared(int arg) {
// 需要AQS子类实现的方法,返回值小于0表示获取同步状态失败
if (tryAcquireShared(arg) < 0)
//进入等待队列
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
//共享模式的节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 前驱节点是head节点,尝试获取同步状态
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 成功将头结点指向自己,如果有剩余资源,唤醒后面的线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 未获取成功,park()进入等待状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
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();
}
}
跟独占模式的acquireQueued比较类似,流程没有太大的区别。唤醒后续节点调用了 doReleaseShared(),是下一节要讲的。
共享式释放锁-releaseShared()
releaseShared()会释放指定arg的资源,如果成功释放并且允许唤醒等待线程,会唤醒等待队列里的后续节点,看源码:
public final boolean releaseShared(int arg) {
// 需要AQS子类实现的方法,返回true表示已经释放资源,允许唤醒后续节点
if (tryReleaseShared(arg)) {
// 唤醒后续节点
doReleaseShared();
return true;
}
return false;
}
接下来再看doReleaseShared()
private void doReleaseShared() {
for (;;) {
// 唤醒操作从头节点开始
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 后续节点需要被唤醒
if (ws == Node.SIGNAL) {
//多个调用入口,需要用cas控制并发
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒
unparkSuccessor(h);
}
// 后续节点不需要被唤醒,将当前节点设置为PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 如果头结点发生变化则重试,否则执行完成,退出
if (h == head)
break;
}
}
4、小结
本文分析了AQS的核心源码,列举了简单的使用,如果配合ReentrantLock、CountDownLatch、Semaphore等的源码来看,更加便于理解。
如果有错误的地方,欢迎讨论和指正。