在介绍 AQS 之前,先一起回顾一下 CAS(Compare And Swap)。
AQS,全名 AbstractQueuedSynchronizer,是一个抽象同步队列,它的内部通过维护一个状态 volatile int state(共享资源的状态),一个 FIFO 线程等待队列来实现同步功能。
Java 并发包很多工具类底层都是基于 AQS 来实现的,比如:Lock 工具 ReentrantLock、栅栏 CountDownLatch、信号量 Semaphore 等。
AQS 内部维护 state 属性,state 用关键字 volatile 修饰,代表着该共享资源的状态一更改就能被所有线程可见,而 AQS 的加锁方式本质上就是多个线程在竞争 state,当 state 为 0 时代表线程可以竞争锁,不为 0 时代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个 FIFO 的等待队列中,这些线程会被 UNSAFE.park() 操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
由 Craig、Landin 和 Hagersten 三位大佬发明,因此命名为 CLH 锁;
是单向链表实现的队列;
是一个自旋公平(FIFO)锁,能确保无饥饿性;
申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱节点释放了锁就结束自旋。
一起看一下线程加锁的过程:
首先获得当前线程的当前节点 curNode,这里每次获取的 CLHNode 节点的 locked 状态都为false;
然后将当前 CLHNode 节点的 locked 状态赋值为 true,表示当前线程的一种有效状态,即获取到了锁或正在等待锁的状态;
注意,为了保证 locked 属性线程间可见,该属性被 volatile 修饰。
线程对 tail 域调用 getAndSet 方法,使自己成为队列的尾部,同时获取一个指向其前趋结点的引用 PreNode;
因为尾指针 tailNode 的总是指向了前一个线程的 CLHNode 节点,因此这里利用尾指针 tailNode 取出前一个线程的 CLHNode 节点,然后赋值给当前线程的前继节点 preNode,并且将尾指针重新指向最后一个节点即当前线程的当前 CLHNode 节点,以便下一个线程到来时使用;
根据前继节点(前一个线程)的 locked 状态判断,若 locked 为 false,则说明前一个线程释放了锁,当前线程即可获得锁,不用自旋等待;若前继节点的 locked 状态为 true,则表示前一线程获取到了锁或者正在等待,自旋等待。
如上图:有3个并发线程同时启动执行 lock 操作,假如3个线程的实际执行顺序为:T1、T2、T3
线程1 过来,执行了 lock 操作,获得了锁,此时 locked 状态为 true
线程2 过来,执行了 lock 操作,由于线程1 还未释放锁,此时自旋等待,locked 状态也为 true
线程3 过来,执行了 lock 操作,由于线程2 处于自旋等待,此时线程3 也自旋等待(因此 CLH 锁是公平锁),locked 状态也为 true
以下为 CLH 锁的释放锁过程:
首先从当前线程的线程本地变量中获取出当前 CLHNode 节点,同时这个 CLHNode 节点被后面一个线程的 preNode 变量指向着;
然后将 locked 状态置为 false 即释放了锁;
注意:locked 因为被 volitile 关键字修饰,此时后面自旋等待的线程的局部变量 preNode.locked 也为 false,因此后面自旋等待的线程结束 while 循环即结束自旋等待,此时也获取到了锁。这一步骤也在异步进行着。
AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配:
AQS 中队列是个双向链表,也是 FIFO 先进先出的特性
通过 head、tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性
head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程
获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改:
// 共享变量,使用 volatile 关键字修饰保证线程可见性
private volatile int state;
资源的可用状态通过 protected 类型的 getState、setState、compareAndSetState
进行操作:
// 返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
// 如果当前状态值等于期望值,则原子地将同步状态设置为给定的更新值。
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS 定义了两种资源共享方式 :独占式(Exclusive)和共享式(Share)。
只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁。
多个线程可以同时执行,如 Semaphore/CountDownLatch。
同步器的设计是基于模板方法模式
的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用) :
使用者继承
AbstractQueuedSynchronizer 并重写
指定的方法(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)。
将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,感兴趣的可以参考:https://refactoringguru.cn/design-patterns/template-method
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:
isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
tryAcquire(int):独占方式
。尝试获取资源,成功则返回 true,失败则返回 false。
tryRelease(int):独占方式
。尝试释放资源,成功则返回 true,失败则返回 false。
tryAcquireShared(int):共享方式
。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式
。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
同步等待队列: 主要用于维护获取锁失败时入队的线程。
条件等待队列: 调用 await() 的时候会释放锁,然后线程会加入到条件队列,调用 signal()/signalAll() 唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。
值为0,初始化状态,表示当前节点在 sync 队列中,等待着获取锁。
CANCELLED,值为 1,表示当前的线程被取消;
SIGNAL,值为 -1,表示当前节点的后继节点包含的线程需要唤醒,也就是 unpark;
CONDITION,值为 -2,表示当前节点在等待 condition,也就是在 condition 队列中;
PROPAGATE,值为 -3,表示当前场景下后续的 acquireShared 能够得以执行;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
AQS 当中的同步等待队列是 CLH 的变体,见:2.1.1、2.1.2,
当前线程如果获取同步状态失败时,AQS 则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到 CLH 同步队列,同时再一次尝试获取锁,如果获取失败则会阻塞当前线程;
当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态;
通过 signal/signalAll 将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)。
AQS 中条件队列是使用单向列表保存的,用 nextWaiter 来连接:
调用 await 方法阻塞线程;
当前线程存在于同步队列的头结点,调用 await 方法进行阻塞(从同步队列转化到条件队列)
调用 Condition#await 方法会释放当前持有的锁,然后阻塞当前线程,同时向 Condition 队列尾部添加一个节点,所以调用 Condition#await 方法的时候必须持有锁。
调用 Condition#signal 方法会将 Condition 队列的首节点移动到阻塞队列尾部,然后唤醒因调用 Condition#await 方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用 Condition#signal 方法的时候必须持有锁,持有锁的线程唤醒被因调用 Condition#await 方法而阻塞的线程。
ReentrantLock 是可重入的独占锁
,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。当我们想要使⽤重⼊锁的时候,使⽤⽅式⼀般是如下3个步骤:
public class ReentrantLockTest {
/** 1. 创建重入锁 */
ReentrantLock lock = new ReentrantLock();
public void doSomething() {
// 2. 加锁 block until condition holds
lock.lock();
try {
// do something
} catch (Exception e) {
// ...
} finally {
// 3. 解锁
lock.unlock();
}
}
}
后面我们就是针对这3个步骤对其源码进⾏解析。在此之前,先一起看一下 Sync、FairSync、NonfairSync 是在哪⾥被使⽤的。
ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
观察源码可以发现 FairSync、NonfairSync 都是继承 Sync,而 Sync 又继承于 AbstractQueuedSynchronizer。
static final class FairSync extends Sync
static final class NonfairSync extends Sync
abstract static class Sync extends AbstractQueuedSynchronizer
ReentrantLock 默认是非公平的:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
当我们使⽤⽆参构造⽅法去创建重⼊锁的时候,底层使⽤的是⾮公平锁
当⼊参 fair 等于 false 的时候,采⽤的就是⾮公平锁 - NonfairSync
当⼊参 fair 等于 true 的时候,采⽤的就是公平锁 - FairSync
公平锁直接就执行 acquire() 排队等待:
/**
* 公平锁
*/
static final class FairSync extends ReentrantLock.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) {
// 和非公平锁相比,这里多了一个是否有线程在等待的判断
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;
}
}
/**
* 非公平锁
*/
static final class NonfairSync extends ReentrantLock.Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 立即执行抢占锁操作。如果抢占成功,则加锁;如果抢占失败,则等待
*/
final void lock() {
// AQS的state如果成功被设置为1,则表示本线程已抢占锁成功;否则,抢占锁失败
if (compareAndSetState(0, 1))
// 将 AOS.excLusive0wnerThread 设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 执行等待
acquire(1);
}
// 是否获得非公平锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
先判断是否有锁,0 是无锁、1 是有锁,等于 0 的话则修改成 1 并且获得锁:
// 如果当前状态值等于期望值 0,则自动将同步状态设置为给定的更新值 1
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
设置当前独占锁的拥有者线程:
protected final void setExclusiveOwnerThread(Thread thread) {
// 将当前线程设置到这把独占锁上
exclusiveOwnerThread = thread;
}
抢锁失败就进行则等待:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
公平锁和非公平锁只有两处不同:
非公平锁在调用 lock 后,首先就会调用 CAS
进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tyAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS
抢锁,但是公平锁会判断等待队列是否有线程处于等待状态
,如果有则不去抢锁,乖乖排到后面。
非公平锁如果这两次 CAS 都不成功,那么后面和公平锁是一样的, 都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态
。
⽆论是公平锁还是⾮公平锁,他们都有机会调⽤相同的⽅法,即:acquire(1)
/**
* 以独占模式获取,忽略中断 (interrupts)
* 通过调用至少一次 tryAcquire(int) 来实现,成功返回。否则线程排队,可能重复阻塞和解除阻塞,调用 tryAcquire(int) 直到成功
* 此方法可用于实现方法 Lock.lock()
*/
public final void acquire(int arg) {
/**
* tryAcquire(1): 进行抢锁操作,返回是否抢锁成功
* addWaiter(Node.EXCLUSIVE): 构建一个独占式节点 Node,维护好该节点的前后指针 Node
* acquireQueued(addWaiter(Node.EXCLUSIVE), 1) 判断获取锁失败的时候,是否应该挂起该线程,如果是,则挂起当前线
*/
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 设置当前线程的中断标识
selfInterrupt();
}
也就是分为三步 tryAcquire(尝试抢锁)、addWaiter(构建节点加入到队列中)、acquireQueued(获取锁失败的时候将线程挂起)
/**
* 进行抢锁操作,返回是否成功
*
* case1> 没人抢占锁(state==0),线程A尝试执行抢占锁操作,如果抢占成功,则返回true; 如果抢占失败,则返回false。
* case2> 有人已经抢占了这个锁(state != 0),但是抢占这个锁的线程就是自己,那么对自己执行重入加锁操作,返回true;如果不是自己抢占的锁,那么返回false。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
/** case1: 如果c等于0,说明可以抢占锁 */
if (c == 0) {
// 如果线程不需要排队 && 抢占锁成功(即:如果state=0,则将该值修改为1,CAS操作成功)
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
/** case2: 如果c不等于0,判断是否是重入操作 (即: 锁本来就是被自己抢占的,支持多次抢占) */
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
通过获取 state 来判断锁是否被获取,等于 0 的话不要排队直接获取锁,可以看到比 NonFairSync 多了 hasQueuedPredecessors 是否需要进入队列排队。
如果不等于 0,判断是否是重入;否则返回 false。
/**
* 主要是用来判断线程需不需要排队。true:线程需要排队。false:线程不需要排队。
* 因为队列是 FIF0 的,所以需要判断队列中有没有相关线程的节点已经在排队了。有则返回true表示线程需要排队;没有则返回 false 表示线程无需排队;
*/
public final boolean hasQueuedPredecessors() {
// 读取头节点
Node t = tail;
// 读取尾节点
Node h = head;
// s是首节点h的后继节点
Node s;
/**
* h != t -> 队列中有 >= 2个节点
* (s = h.next) == null -> 头节点没有后继节点,即:只有自己一个node或者创建了h的后置节点,但是还没有执行h.next=node
* s.thread != Thread.currentThread() -> 第二个节点承载的线程不是当前线程
*/
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
/**
* 是否获得非公平锁
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
当前线程尝试获取锁,该方法是可重入的实现;
如果此时没有其他线程占有锁,或者,当前占有锁的线程就是当前线程,那么就返回true,表明获取到锁;
否则返回false,表示没有获取到锁。
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 拿到state状态
int c = getState();
// 如果是0说明没有其他线程持有锁,则当前线程有机会对其赋值
if (c == 0) {
// 如果赋值成功,则表示当前线程持有锁(此处采用CAS),对state加1,表示当前线程第一次持有锁,注意acquires=1
if (compareAndSetState(0, acquires)) {
// 将当前线程赋值到exclusiveOwnerThread字段,其他地方可以通过该字段判断是哪个线程在持有锁
setExclusiveOwnerThread(current);
// 返回true,表示加锁成功
return true;
}
}
// 如果不是0,则说明有线程占用锁了,那么判断占有锁的线程是不是当前线程,如果是,则可重入(Reentrant)
else if (current == getExclusiveOwnerThread()) {
// 因为再次持有锁(重入进来的),所以此处对state赋值,state是几,就表示当前是第几次再次获得锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果以上都不是,则没有获取到锁
return false;
}
addWaiter() 方法用于创建并添加等待节点。
首先创建一个节点node,创建了当前节点,跟一个独立节点,再拿到tail尾部指针;
如果tail不为空,将node的上一个节点只指向tail,更新尾部节点为新node,再将tail.next指向node;
尾部指针为空的是直接执行enq。
private Node addWaiter(Node mode) {
// 首先,以共享或者独占模式创建节点
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加到队列末尾
enq(node);
return node;
}
这里涉及到一个 enq()
方法,这个方法不复杂,主要是自旋初始化 AQS 中的头结点和尾节点,值得注意的是,这里的头结点实际上是一个哨兵节点,本身并无意义,当等待队列排队获取资源的时候,会直接从 head.next 开始。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果是空队列,则初始化一个空内容的节点node,作为头节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
// 如果不是空队列,将node节点放到链表的尾部
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
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);
}
}
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 判断是否可以进行释放锁操作
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 执行unpark操作
unparkSuccessor(h);
return true;
}
return false;
}
无论是公平锁还是非公平锁都会直接执行release
protected final boolean tryRelease(int releases) {
// 可重入锁,减去一次持锁次数
int c = getState() - releases;
// 如果当前线程不是持有锁的线程则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果可重入次数为0,说明确实释放锁了
if (c == 0) {
free = true;
// 独占线程设置为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
这里就是让 tryRelease()
去执行释放锁的过程,换句话说,就是改变 state
。
unparkSuccessor()
方法的主要用途是
在前驱节点(其实就是等待队列的头结点)释放锁后,去唤醒等待队列中的后继节点;
如果后继节点处于 CANCELLED 状态,说明该节点已经挂掉了,就从尾节点向前找到离后继节点最近的节点去唤醒,否则直接唤醒后继节点。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 如果头节点状态还处于等待状态,则改回初始状态
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果后继节点存在并被标记为CANCELLED状态
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点开始,找到离node最近的处于等待状态的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒节点
LockSupport.unpark(s.thread);
}