AQS
一、AQS概述
AQS 全名 AbstractQueuedSynchronizer,意为抽象队列同步器,JUC(java.util.concurrent 包)下面的 Lock 和其他一些并发工具类都是基于它来实现的。AQS 维护了一个 volatile 的 state 和一个 CLH(FIFO)双向队列
二、分析
2.1、state
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改
private volatile int state; // 共享变量,使用 volatile 修饰保证线程可见性
状态信息通过 protected 类型的 方法进行操作
- getState()
- setState()
- compareAndSetState()
getState:返回同步状态的当前值
protected final int getState() {
return state;
}
setState:设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
compareAndSetState:原子地(CAS操作)将同步状态值设置为给定值 update 如果当前同步状态的值等于 expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2、AQS对资源的共享方式
1、Exclusive(独占)
只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍:
- 公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
2、Share(共享)
多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了
2.3、AQS 底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
- 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:
// 独占方式。尝试获取资源,成功则返回 true,失败则返回 false
protected boolean tryAcquire(int)
// 独占方式。尝试释放资源,成功则返回 true,失败则返回 false
protected boolean tryRelease(int)
// 共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
protected int tryAcquireShared(int)
// 共享方式。尝试释放资源,成功则返回 true,失败则返回 false
protected boolean tryReleaseShared(int)
// 该线程是否正在独占资源。只有用到 condition 才需要去实现它
protected boolean isHeldExclusively()
什么是钩子方法呢? 钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现
除了上面提到的钩子方法之外,AQS 类中的其他方法都是 final ,所以无法被其他类重写
以 ReentrantLock 为例
state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state + 1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的
再以 CountDownLatch 以例
任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown() 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
2.4、CLH(FIFO)队列
AQS 中是通过内部类 Node 来维护一个 CLH 队列的。源码如下:
static final class Node {
// 标记共享式访问
static final Node SHARED = new Node();
// 标记独占式访问
static final Node EXCLUSIVE = null;
// 字段 waitStatus 的值,表示当前节点已取消等待
static final int CANCELLED = 1;
// 字段 waitStatus 的值,表示当前节点取消或释放资源后,通知下一个节点
static final int SIGNAL = -1;
// 表示正在等待触发条件
static final int CONDITION = -2;
// 表示下一个共享获取应无条件传播
static final int PROPAGATE = -3;
//Node对象存储标识的地
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) {
// 用于Condition使用
this.waitStatus = waitStatus;
this.thread = thread;
}
}
三、ReentrantLock
下面来看 ReentrantLock 中相关的源代码:
ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)
3.1、非公平锁
1、非公平锁演示
@Test
public void testUnfairLock() throws InterruptedException {
// 无参构造函数,默认创建非公平锁模式
ReentrantLock reentrantLock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
final int threadNum = i;
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("线程" + threadNum + "获取锁");
Thread.sleep(1000);
} catch (InterruptedExce