本篇文章对Java中的AbstractQueuedSynchronizer(AQS)进行分析和学习。若有不正之处请多多谅解,并欢迎批评指正。
为叙述方便,下文都以AQS替代AbstractQueuedSynchronizer。
使用的Java版本
src git:(master) ✗ java -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)
AQS是干什么的呢?
下面是AQS类的部分介绍,咱也看不懂,只能用百度翻译一下哈哈,建议英文好的直接看源码里的类注释。
AQS提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(semaphores、events等)。对于大多数依赖单个原子{@code int}值来表示状态的同步器来说,这个类是一个有用的基础。子类必须定义改变这个状态的受保护的方法,以及定义这个状态在对象被获取或释放时的含义。鉴于这些,AQS类中的其他方法执行所有排队和阻塞机制。
子类应该被定义为非公共的内部辅助类,用于实现其封闭类的同步属性。AbstractQueuedSynchronizer类不实现任何同步接口。相反,它定义了例如{@link #acquireInterruptibly}这样的方法,可以根据具体的锁和相关的同步器来适当地调用,以实现它们的公共方法。
一句话:本篇文章只需要知道AQS可以用来实现锁即可。
我们一般不会直接使用AQS,所以我们以ReentrantLock(可重入锁)来引出AQS。明白了AQS就明白了ReentrantLock是如何获取锁以及释放锁的了。
先说一下大致流程:
Java中的ReentrantLock的获取锁和释放锁是通过AQS来实现的。
AQS内部维护了一个int类型的值来表示
同步状态
和一个先进先出(FIFO)的等待队列
。
/**
* 同步状态
*/
private volatile int state;
/**
* 等待队列的head,延迟初始化。除了初始化之外,head只能通过setHead方法来修改。
* 注意,如果head存在可以保证head的waitStatus不是CANCELLED.
*/
private transient volatile Node head;
/**
* 等待队列的尾,惰性初始。只有在使用enq方法添加新的等待节点的时候修改。
*/
private transient volatile Node tail;
- 对于非公平锁,线程总是会先尝试获取锁,如果获取成功就直接执行,如果获取失败会进入等待队列。进入等待队列中的线程会休眠,等待被唤醒。
- 对于公平锁,如果已经有线程在等待获取锁了,那么新的线程就会直接排在等待队列后面等待获取锁。
- 持有锁的线程执行完毕释放锁,唤醒等待队列中的线程。
- 线程被唤醒后会尝试获取锁,如果成功获取锁那么线程就执行,否则线程会再次休眠等待被唤醒。
我们在使用ReentrantLock的过程中,既可以构建一个使用非公平策略的ReentrantLock实例,也可以构建一个使用公平策略的ReentrantLock实例。
ReentrantLock的类结构
public class ReentrantLock implements Lock, java.io.Serializable {
//Sync成员变量
private final Sync sync;
//AQS的子类
abstract static class Sync extends AbstractQueuedSynchronizer {
}
//非公平策略
static final class NonfairSync extends Sync {
}
//公平策略
static final class FairSync extends Sync {
}
}
我们看到ReentrantLock类中有一个Sync类型的成员变量,Sync类继承了AQS,然后
NonfairSync和FairSync都继承了Sync,分别实现非公平锁和公平锁。
ReentrantLock的构造函数
public ReentrantLock() {
//使用非公平策略
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
//使用公平策略
sync = fair ? new FairSync() : new NonfairSync();
}
我们可以选择构建公平的或非公平的ReentrantLock实例,ReentrantLock中获取锁和释放锁相关的方法如下所示。我们先看非公平锁的情况。
void lock();
boolean tryLock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
非公平的ReentrantLock
ReentrantLock的lock方法
public void lock() {
sync.lock();
}
ReentrantLock的lock方法内调用了sync的lock方法。NonfairSync的实现如下所示。
NonfairSync的lock方法
final void lock() {
//注释1处,
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//注释2处
acquire(1);
}
注释1处,首先调用AQS的compareAndSetState方法以CAS的方式修改AQS的state
变量,如果修改成功,说明当前线程成功获取了锁,然后将当前线程设置为锁的持有者。注意是以独占模式持有锁的。
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
如果修改AQS的state
变量失败,说明此时有其他线程已经持有了锁,那么就调用acquire(int arg)
方法获取锁,注意我们传入的参数是1。
AQS的acquire(int arg)方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//标记为独占模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个步骤可以分为3步(把大象放进冰箱里需要几步?)
步骤1: 调用tryAcquire(arg) 尝试获取锁,获取成功直接返回
步骤2: 尝试获取锁失败将当前线程以独占锁的方式加入等待队列
步骤3: 为已经加入队列中的线程尝试获取锁
步骤1:调用tryAcquire(arg) 尝试获取锁
AQS没有实现这个方法,需要子类来实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我们看下ReentrantLock.NonfairSync类的实现
protected final boolean tryAcquire(int acquires) {
//调用了父类ReentrantLock.Sync的nonfairTryAcquire(acquires)方法
return nonfairTryAcquire(acquires);
}
ReentrantLock.Sync的nonfairTryAcquire(acquires)方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取同步状态值
int c = getState();
//注释1处
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//注释2处
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//注释3处
return false;
}
在注释1处,如果同步状态值为0,说明没有线程持有锁,那么就以CAS的方式修改AQS的state变量,如果修改成功,说明当前线程成功获取了锁,然后将当前线程设置为锁的持有者,然后返回true,获取锁成功。
注释2处,如果有线程持有锁,并且持有锁的线程是当前线程,那么就将同步状态值加1然后重新赋值给同步状态值state,然后返回true,获取锁成功。
AQS的setState(int newState)方法
protected final void setState(int newState) {
state = newState;
}
注意:调用这个方法的前提是当前线程就是锁的持有者,所以可以修改state值,并不需要方法同步。
注释3处,获取锁失败。
到此步骤1结束,如果步骤1中获取锁失败,就会进入步骤2。
步骤2: 获取失败将当前线程加入等待队列
在这里我们要提一下AQS的一个内部类Node。Node类是对每一个等待获取锁的线程的封装,其包含了线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。还包括指向当前节点的前驱节点的指针和后继节点的指针(双向链表)。Node类的成员变量waitStatus则表示当前Node节点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
//默认是0
volatile int waitStatus;
CANCELLED:表示当前节点由于超时或者中断而被取消。进入该状态后的节点状态将不会再变化。特别的,取消节点的线程不会被再次阻塞。
SIGNAL:当前节点的后继节点被阻塞了,所以当前节点在释放锁或者取消的时候必须唤醒后继节点。后继节点入队时,会将父节点的状态更新为SIGNAL。
CONDITION:表示节点正在一个条件队列中,本篇文章暂时忽略。
PROPAGATE:共享模式下,节点不仅会唤醒其后继节点,同时也可能会唤醒后继节点的后继节点。比如当前节点释放了10个资源,当前节点的后继节点只需要6个节点,那么当前节点在释放的时候就会唤醒后继节点和后继节点的后继节点。
0:新节点进入等待队列时的默认状态。
注意,负值表示节点处于有效等待状态,而正值表示节点已被取消。所以源码中很多地方用>0、<0来判断节点的状态是否正常。
private Node addWaiter(Node mode) {
//以独占模式加入等待队列
Node node = new Node(Thread.currentThread(), mode);
// 先尝试最快的入队方式
Node pred = tail;
//注释1处
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//注释2处
enq(node);
return node;
}
在注释1处,如果尾节点不为null,就直接将当前节点使用CAS的方式更新为尾节点,如果更新成功就返回node,这是最快的入队方式。
如果尾节点为null,或者将当前节点使用CAS的方式更新为尾节点失败,就调用注释2处的enq(final Node node)
方法将node加入队列。
AQS的enq(final Node node)方法,注意,这个方法是一个无限循环,只有成功将加入到队列尾部才会返回。
private Node enq(final Node node) {
for (;;) {//有一点疑问,这个for循环什么时候退出呢?
Node t = tail;
if (t == null) { // 如果队列不存在,就新建一个node然后初始化队列
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
首先如果尾节点为null,说明队列此时还不存在,就新建一个节点然后以CAS的方式将新创建的节点设置为头节点,如果成功则让尾节点也指向node。如果如果尾节点不为null,就以CAS的方式将node更新为尾节点。
注意,这个方法是一个无限循环,只有成功将node加入到队列尾部才会返回。
将node加入到等待队列成功以后会进入到AQS的acquire(int arg)方法的步骤3
步骤3: 为已经加入队列中的线程尝试获取锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功获取锁
try {
boolean interrupted = false;//标记线程是否被中断
//无限循环
for (;;) {//注释1处
//获取前驱节点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//如果前驱节点是head并且尝试获取锁成功,就将当前节点更新为head节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//注释2处
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//标记线程在阻塞过程中是否被中断
interrupted = true;
}
} finally {//注释3处
if (failed)
cancelAcquire(node);
}
}
注释1处,如果前驱节点是head并且调用tryAcquire(int arg)
方法获取锁成功,就将当前节点更新为head节点,然后返回。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
在setHead方法中将node的thread和prev变量都置为了null,是为了帮助GC和避免不必要的唤醒和遍历。
在注释2处,如果获取锁失败后则判断是否应该阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* node节点已经设置了状态告诉前驱节点在释放锁的时候通知自己,所以node节点可以被安全的阻塞。
*/
return true;
if (ws > 0) {
/*
* 前驱节点已经被取消了,向前寻找状态有效的前驱节点,然后将node设置为有效前驱节点的后继节点。
* 注意:已经被取消的节点会被GC,这些节点相当于一个无引用链。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 以CAS的方式更新前驱节点的waitStatus为Node.SIGNAL,告诉前驱节点在释放锁的时候通知自己。
* 可能会失败。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果shouldParkAfterFailedAcquire方法返回false,那么重新循环。如果返回true则调用parkAndCheckInterrupt方法。
AQS的parkAndCheckInterrupt()方法,注意这个方法会阻塞线程,并在线程 被唤醒后,通过调用Thread.interrupted()返回在阻塞过程中线程是否被中断。
private final boolean parkAndCheckInterrupt() {
//注释1处
LockSupport.park(this);
//唤醒后,返回在阻塞过程中是否被中断
return Thread.interrupted();
}
LockSupport.park(this);
注释1处这行代码会阻塞当前线程,Thread.interrupted()这行代码就不会执行了,只有被唤醒后Thread.interrupted()这行代码才会执行。
在线程被唤醒后,返回在阻塞过程中是否被中断。注意Thread.interrupted()
方法会将线程的中断状态清空。
当线程被唤醒后,也会重新循环。
到现在AQS的acquire方法就结束了。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//中断自己
selfInterrupt();
}
这里要注意一下,因为Thread.interrupted()
方法会将线程的中断状态清空,所以我们这里要判断一下,如果线程在阻塞过程中被中断了,我们在这里要调用selfInterrupt()
方法来中断当前线程,也就是将当前线程的中断状态置为true。
现在总结一下AQS的acquire(int arg)方法的流程。
- 调用子类的
tryAcquire(int acquires)
方法先尝试获取锁,如果成功则直接返回; - 获取失败,则调用
addWaiter(Node mode)
方法将该线程加入等待队列的尾部,并标记为独占模式; - 将该线程加入等待队列后,调用
acquireQueued(final Node node, int arg)
方法来尝试获取锁,在这个过程中,线程可能会被多次阻塞、唤醒。如果成功获取锁,就将当前节点更新为head节点,然后返回。如果在整个等待过程中被中断过,则返回true,否则返回false。 - 如果线程在等待过程中被中断过,它是不响应的。只是获取锁后才再进行自我中断
selfInterrupt()
,将中断补上。
到此,非公平的ReentrantLock的lock()
方法分析完毕。
boolean tryLock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
这三个获取锁的方法过程也是类似的,就不进行分析了,接下来看一看非公平的ReentrantLock释放锁的过程。
ReentrantLock的unlock()方法
public void unlock() {
//调用AQS的release方法
sync.release(1);
}
其实我们这里可以看到,一个线程可以多次获取锁(可重入锁),每获取一次锁就会将state加1,每释放一次锁,就会将state减1,当前线程将state减到0的时候,说明当前线程释放了锁。
AQS的release(int arg)方法
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(int arg)方法,AQS没有实现这个方法,我们直接看ReentrantLock.Sync的实现
protected final boolean tryRelease(int releases) {
//同步状态每次减1
int c = getState() - releases;
//如果当前线程不是锁的持有者,抛出异常,没资格释放锁,哈哈
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//同步状态为0,表示成功释放了锁
free = true;
setExclusiveOwnerThread(null);
}
//更新同步状态的值
setState(c);
return free;
}
方法首先将同步状态值减去1,如果如果当前线程不是锁的持有者,抛出异常。如果同步状态值减到了0,说明表示成功释放了锁,然后我们将锁的持有者设置为null,最后更新同步状态值,然后返回。
如果tryRelease返回了false,说明没有成功释放锁,如果返回true,表示成功释放了锁,那么我们要唤醒后继节点。
private void unparkSuccessor(Node node) {
/*
* 首先,它会检查节点的waitStatus字段。如果waitStatus小于0
*(表示节点的后继节点需要唤醒),那么它会尝试将waitStatus设置为0。
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 然后,它会找到需要唤醒的节点。这个节点通常是当前节点的直接后继节点,
* 但是如果后继节点被取消或者为null,那么它会从队列的尾部开始向前遍历,
* 找到第一个未被取消的节点。
*/
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;
}
//最后,如果找到了需要唤醒的节点,那么它会调用LockSupport.unpark方法来唤醒节点对应的线程。
if (s != null)
LockSupport.unpark(s.thread);
}
被唤醒的线程会从上面的parkAndCheckInterrupt方法中第二行代码恢复执行
private final boolean parkAndCheckInterrupt() {
//注释1处
LockSupport.park(this);
//唤醒后,在这里恢复执行,返回在阻塞过程中是否被中断
return Thread.interrupted();
}
总结一下AQS的release(int arg)方法的流程。
release方法每次释放锁就会将state值减1,如果彻底释放了(即state==0),就会唤醒等待队列里的其他线程来获取锁。
看完了非公平的ReentrantLock获取锁和释放锁的过程,接下来我们看看公平的ReentrantLock获取锁和释放锁过程。
公平的ReentrantLock
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
当我们使用上面的构造函数创建ReentrantLock实例的时候,如果传入的参数是true,那么构建的是公平的ReentrantLock
public void lock() {
sync.lock();
}
ReentrantLock.FairSync的lock方法
final void lock() {
//获取锁
acquire(1);
}
AQS的acquire(int arg)方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个步骤和非公平策略获取锁是一样的可以分为3步
步骤1: 调用tryAcquire(arg) 尝试获取锁,获取成功直接返回
步骤2: 获取失败将当前线程以独占锁的方式加入等待队列
步骤3: 为已经加入队列中的线程尝试获取锁
步骤1: 调用tryAcquire(arg) 尝试获取锁
ReentrantLock.FairSync的tryAcquire(int acquires)方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//注释1处
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//注释2处
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在上面方法的注释1处,c==0表示没有线程持有锁,首先调用hasQueuedPredecessors
方法判断等待队列里面是否有节点,如果有等待节点,返回true,那么当前线程就不去获取锁(体现了公平)。如果没有等待节点并且以CAS的方式获取锁成功则将当前线程赋值为持有锁的线程,返回true。
在注释2处,c!=0表示有线程持有锁,如果是当前线程持有锁的话,那么就将同步状态值加1,返回true。
步骤2和步骤3和非公平的ReentrantLock是一样的,就不再赘述了。
公平的ReentrantLock和非公平的ReentrantLock的release(int arg)
方法也是一样的就不再赘述了。
公平的ReentrantLock和非公平的ReentrantLock的差异
- 公平的ReentrantLock和非公平的ReentrantLock的差异由
ReentrantLock.FairSync
和ReentrantLock.NonfairSync
体现。非公平锁总会先尝试获取锁。如下图所示:左边是公平锁,右边是非公平锁。
lock方法
tryAcquire 方法
- 如果等待队列里有节点等待获取锁,公平的ReentrantLock就会直接进入等待队列排队。非公平的ReentrantLock无论等待队列里是否有节点等待获取锁,总是先尝试获取锁,如果获取失败才进入等待队列进行排队。
结尾:本篇文章通过ReentrantLock引出了AQS是如何帮助ReentrantLock实现的获取锁和释放锁的。下一篇文章打算分析一下AQS是如何帮助ReentrantLock实现Condition
功能的。
参考链接: