AQS
是JUC
的基石。很多
JUC
相关的工具类其内部就用到了AQS
,而这些工具类已经基本足够覆盖大部分的业务场景了,这就使得我们即便不了解AQS
,也能利用这些工具类顺利进行开发。我们学习
AQS
的目的主要是想理解其背后的原理、学习设计思想,以提高技术并应对面试。
1、什么是 AQS
AQS 的重要性
可以先看看 AQS
应用在了哪些地方,虽然我们从未直接的去使用过它:
如图所示,AQS
在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8
),AQS
是这些类的底层原理。
我觉得这就是封装思想的体现。比如
HashMap
,内部不就封装了一个Node
节点来存储键值对,Lock
接口我们用的时候就lock()、unlock()
方法加锁解锁,其实它的底层调用Sync
的相关方法,而Sync
又继承AQS
。我觉得封装很重要的一个特点,内部的细节对我们是不可见的。
锁和协作类有共同点
阀门功能
ReentrantLock
和 Semaphore
,二者之间有没有什么共同点?
其实它们都可以当做一个阀门来使用。比如我们把 Semaphore 的许可证数量设置为 1,那么由于它只有一个许可证,所以只能允许一个线程通过,并且当之前的线程归还许可证后,会允许其他线程继续获得许可证。
其实这点和 ReentrantLock
很像,只有一个线程能获得锁,并且当这个线程释放锁之后,会允许其他的线程获得锁。那如果线程发现当前没有额外的许可证时,或者当前得不到锁,那么线程就会被阻塞,并且等到后续有许可证或者锁释放出来后,被唤醒,所以这些环节都是比较类似的。
为什么需要 AQS
锁和协作类,它们有很多工作是类似的,所以如果能把实现类似工作的代码给提取出来,变成一个新的底层工具类(或称为框架)的话,就可以直接使用这个工具类来构建上层代码了,而这个工具类其实就是 AQS
。
如果没有 AQS
如果没有 AQS,那就需要每个线程协作工具类自己去实现至少以下内容,包括:
- 状态的原子性管理
- 线程的阻塞与解除阻塞
- 队列的管理
这里的状态对于不同的工具类而言,代表不同的含义,比如对于 ReentrantLock
而言,它需要维护锁被重入的次数,但是保存重入次数的变量是会被多线程同时操作的,就需要进行处理,以便保证线程安全。不仅如此,对于那些未抢到锁的线程,还应该让它们陷入阻塞,并进行排队,并在合适的时机唤醒。所以说这些内容其实是比较繁琐的,而且也是比较重复的,而这些工作目前都由 AQS
来承担了。
哪有什么岁月静好,不过是有人替你负重前行。
AQS 的作用
AQS 是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS
以后,很多用于线程协作的工具类就都可以很方便的被写出来,有了 AQS
之后,可以让更上层的开发极大的减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题。
2、AQS 内部原理
AQS
最核心的三大部分就是状态、队列和期望协作工具类去实现的获取/释放等重要方法。
state 状态
如果 AQS
想要去管理或者想作为协作工具类的一个基础框架,那么它必然要管理一些状态,而这个状态在 AQS
内部就是用 state
变量去表示的。
/**
* The synchronization state.
*/
private volatile int state;
而 state 的含义并不是一成不变的,它会根据具体实现类的作用不同而表示不同的含义。
(1)Semaphore
在信号量里面,state
表示的是剩余许可证的数量。如果我们最开始把 state
设置为 10,这就代表许可证初始一共有 10 个,然后当某一个线程取走一个许可证之后,这个 state
就会变为 9,所以信号量的 state
相当于是一个内部计数器。
(2)CountDownLatch
在 CountDownLatch
工具类里面,state 表示的是需要“倒数”的数量。一开始我们假设把它设置为 5,当每次调用 CountDown
方法时,state
就会减 1,一直减到 0 的时候就代表这个门被打开。
(3)ReentrantLock
在 ReentrantLock
中它表示的是锁的占有情况。最开始是 0,表示没有任何线程占有锁;如果 state
变成 1,则就代表这个锁已经被某一个线程所持有了。
那为什么还会变成 2、3、4 呢?为什么会往上加呢?因为 ReentrantLock
是可重入的,同一个线程可以再次拥有这把锁就叫重入。
如果这个锁被同一个线程多次获取,那么 state
就会逐渐的往上加,state
的值表示重入的次数。在释放的时候也是逐步递减,只有当它减到 0 的时候,此时恢复到最开始的状态了,则代表现在没有任何线程持有这个锁了。
state
等于 0 表示锁不被任何线程所占有,代表这个锁当前是处于释放状态的,其他线程此时就可以来尝试获取了。
state 并发修改问题:
因为 state
是会被多个线程共享的,会被并发地修改,所以所有去修改 state 的方法都必须要保证 state 是线程安全的。可是 state 本身它仅仅是被 volatile
修饰的,volatile
本身并不足以保证线程安全
AQS
中两个修改 state
的方法源码:
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
(1)当对基本类型的变量进行直接赋值时,如果加了 volatile
就可以保证它的线程安全。注意,这是 volatile
的非常典型的使用场景。
(2)利用了 Unsafe
里面的 CAS
操作,利用 CPU
指令的原子性保证了这个操作的原子性。
总结:
在 AQS
中有 state
这样的一个属性,是被 volatile
修饰的,会被并发修改,它代表当前工具类的某种状态,在不同的类中代表不同的含义。
FIFO 队列
FIFO 队列,即先进先出队列,这个队列最主要的作用是存储等待的线程。
假设很多线程都想要同时抢锁,那么大部分的线程是抢不到的,那怎么去处理这些抢不到锁的线程呢?就得需要有一个队列来存放、管理它们。AQS
的一大功能就是充当线程的“排队管理器”。
当多个线程去竞争同一把锁的时候,就需要用排队机制把那些没能拿到锁的线程串在一起;而当前面的线程释放锁之后,这个管理器就会挑选一个合适的线程来尝试抢刚刚释放的那把锁。
AQS
就一直维护这个队列,并把等待的线程都放到队列里面。
队列内部是双向链表的形式,其数据结构看似简单,但是要想维护成一个线程安全的双向队列却非常复杂,因为要考虑很多的多线程并发问题。
在队列中,分别用 head
和 tail
来表示头节点和尾节点,两者在初始化的时候都指向了一个空节点。头节点可以理解为“当前持有锁的线程”,而在头节点之后的线程就被阻塞了,它们会等待被唤醒,唤醒也是由 AQS
负责操作的。
获取/释放方法
获取和释放相关的重要方法,这些方法是协作工具类的逻辑的具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。
获取方法
获取操作通常会依赖 state
变量的值,根据 state
值不同,协作工具类也会有不同的逻辑,并且在获取的时候也经常会阻塞。
(1)ReentrantLock
中的 lock
方法就是其中一个“获取方法”,执行时,如果发现 state
不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。
(2)Semaphore
中的 acquire
方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state
的值。如果 state
值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state
是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态。
(3)CountDownLatch
获取方法就是 await
方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await
的时候会判断 state
的值,如果 state
不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state
减为 0,此时就代表现在这个门放开了,所以之前阻塞的线程就会被唤醒。
释放方法
获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。
(1)在 Semaphore 信号量里面,释放就是 release
方法(包含重载方法),release()
方法的作用是去释放一个许可证,会让 state
加 1;
(2)在 CountDownLatch
里面,释放就是 countDown
方法,作用是倒数一个数,让 state
减 1。
3、AQS 应用原理
AQS 用法
如果想使用 AQS
来写一个自己的线程协作工具类,通常而言是分为以下三步,这也是 JDK
里利用 AQS 类的主要步骤:
- 第一步,新建一个自己的线程协作工具类,在内部写一个
Sync
类,该Sync
类继承AbstractQueuedSynchronizer
,即AQS
; - 第二步,想好设计的线程协作工具类的协作逻辑,在
Sync
类里,根据是否是独占,来重写对应的方法。如果是独占,则重写tryAcquire
和tryRelease
等方法;如果是非独占,则重写tryAcquireShared
和tryReleaseShared
等方法; - 第三步,在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用
AQS
对应的方法,如果是独占则调用acquire
或release
等方法,非独占则调用acquireShared
或releaseShared
或acquireSharedInterruptibly
等方法。
为什么不以接口的方式实现,而是以重写的方式?
AQS
的原作者 Doug Lea
的论文中已经进行了说明,他认为如果是实现接口的话,那每一个抽象方法都需要实现。
实际上我们并不是每个方法都需要重写,根据需求的不同,有选择的去实现一部分就足以了,所以就设计为不采用实现接口,而采用继承类并重写方法的形式。
相关方法源码,可以发现代码就直接抛出异常,所以是需要我们自己去重写逻辑的:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
AQS 在 CountDownLatch 的应用
在 CountDownLatch 里面有一个子类,该类的类名叫 Sync,这个类正是继承自 AQS。
源码如下:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
Sync
类继承了 AQS
,这正是第一步。
还重写了 tryAcquireShared 和 tryReleaseShared 方法,这是第二步。
构造函数
源码如下:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
小于 0 抛出异常,大于 0 是调用了 Sync 类的构造方法,源码为:
Sync(int count) {
setState(count);
}
我们发现调用了 setState()
方法,这个方法其实是 AQS 的方法,源码为:
protected final void setState(int newState) {
state = newState;
}
通过 CountDownLatch
构造函数将传入的 count
最终传递到 AQS 内部的 state 变量,给 state
赋值,state
就代表还需要倒数的次数。
这也验证了 AQS
是 JUC
的基石。
getCount
该方法的作用是获取当前剩余的还需要“倒数”的数量。源码如下:
public long getCount() {
return sync.getCount();
}
调用了 Sync
的 releaseShared
方法,源码为:
int getCount() {
return getState();
}
最终还是调用了 AQS
的 getState
方法,源码为:
protected final int getState() {
return state;
}
所以最终它获取到的就在 AQS
中 state
变量的值。
countDown
该方法其实就是 CountDownLatch
的“释放”方法,源码为:
public void countDown() {
sync.releaseShared(1);
}
调用了 Sync
的 releaseShared
方法,源码为:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
Sync
的 tryReleaseShared
方法源码如下:
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
doReleaseShared
方法,唤醒所有线程:
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
await
方法是 CountDownLatch
的“获取”方法,调用 await
方法会把线程阻塞,直到倒数为 0 才能继续执行。
源码为:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
Sync
-> AQS
,最终是调用了 AQS
的 acquireSharedInterruptibly
方法:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
tryAcquireShared
方法源码:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}