0%

第6章:AQS 抽象同步器的核心原理

前面介绍在激烈争用的情况下,CAS 自旋实现的轻量级锁会有两大问题:

  • CAS 恶性空自旋会浪费大量 CPU 资源
  • 某些架构 CPU 上可能会导致 “总线风暴“

解决这些问题的常见方案有 2 种:

  • 分散操作热点
  • 使用队列削峰

JUC 使用队列削峰方案解决 CAS 性能问题,提供了一个基于双向队列的削峰基类——抽象基础类 AbstractQueuedSynchronizer(抽象同步器类,简称 AQS) 。JUC 中许多类都是基于AQS构建:例如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、FutureTask等。

6.1 锁与队列的关系

第5章介绍了 CLH ,它用了FIFO的单项队列;AQS 是 CLH 的一个变种,主要原理差不多,用的是 FIFO 的双向链表,这样做的好处就是可以从任意一个节点开始很方便地访问前驱和后继节点

6.2 AQS 的核心成员

6.2.1 状态标志位

AQS 中维持了一个单一的 volatile 修饰 int 类型的状态信息 state ,它标记了锁的状态,默认初始状态 0 为未锁定状态。同时,提供了 compareAndSetState 原子设置方法来设置 state 的值。

当线程 A 通过 tryAcquire() 获取到独占锁并将 state 加一后,其他线程通过 tryAcquire 获取锁就会失败(执行compareAndSet(0,1)会失败),直到 A 释放了锁为止,其他线程才能获取锁。

AQS 继承了 AbstractOwnableSynchronizer ,父类中有个当前占用该锁的线程的变量 exclusiveOwnerThread:

1
2
//表示当前占用该锁的线程
private transient Thread exclusiveOwnerThread;

6.2.2 队列节点类 Node

AQS 是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系,Node 的主要成员如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static final class Node {

//节点状态:值为SIGNAL、CANCELLED、CONDITION、PROPAGATE、0
//普通的同步节点的初始值为0,条件等待节点的初始值为CONDITION(-2)
volatile int waitStatus;

//节点所对应的线程,为抢锁线程或者条件等待线程
volatile Thread thread;

//前驱节点,当前节点会在前驱节点上自旋,循环检查前驱节点的waitStatus状态
volatile Node prev;

//后继节点
volatile Node next;

//若当前Node不是普通节点而是条件等待节点,则节点处于某个条件的等待队列上
//此属性指向下一个条件等待节点,即其条件队列上的后继节点
Node nextWaiter;
...
}

解释一下 waitStatus 变量中的几个值:

  • CONDITION :waitStatus 取这个值时,表示该线程(调用了Condition 的 awati 方法后)在条件队列中阻塞(Condition 有使用),当持有锁的线程调用了 Condition 的 signal() 方法后,节点会从该 Condition 的等待队列转移到该锁的同步队(也就是AQS的FIFO双向队列)列中去竞争锁。
  • PROPAGATE:waitStatus 取这个值时,表示下一个线程获取共享锁后,自己的共享状态会被无条件传播下去,因为共享锁可能出现有N个锁可用,这时直接让后面 N 个节点都来工作。这种状态在 CountDownLatch 中用到了

6.3.1 模板模式

这种模式值得看下,AQS 也使用这种模式

6.4 通过 AQS 实现一把简单的独占锁

基于 AQS 实现一个简单的非公平的独占锁 SimpleMockLock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class SimpleMockLock implements Lock {
//同步器实例
private final Sync sync = new Sync();

// 自定义的内部类:同步器
// 直接使用 AbstractQueuedSynchronizer.state 值表示锁的状态
// AbstractQueuedSynchronizer.state=1 表示锁没有被占用
// AbstractQueuedSynchronizer.state=0 表示锁没已经被占用
private static class Sync extends AbstractQueuedSynchronizer {
//钩子方法
protected boolean tryAcquire(int arg) {
//CAS更新状态值为1
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

//钩子方法
protected boolean tryRelease(int arg) {
//如果当前线程不是占用锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread()) {
//抛出非法状态的异常
throw new IllegalMonitorStateException();
}

//如果锁的状态为没有占用
if (getState() == 0) {
//抛出非法状态的异常
throw new IllegalMonitorStateException();
}
//接下来不需要使用CAS操作,因为下面的操作不存在并发场景
setExclusiveOwnerThread(null);
//设置状态
setState(0);
return true;
}

//显式锁的抢占方法
@Override
public void lock() {
//委托给同步器的acquire()抢占方法
sync.acquire(1);
}

//显式锁的释放方法
@Override
public void unlock() {
//委托给同步器的release()释放方法
sync.release(1);
}
// 省略其他未实现的方法
}
}

6.5 AQS 锁抢占的原理

文中前面讲了一大堆,实在没法梳理各个章节的联系,云里雾里的,直到用 ReentrantLock 来讲这个过程,就清晰了,所以前面一些内容略过。

直接以 ReentrantLock 抢锁来说明整个抢锁流程,ReentrantLock 有2种模式:公平锁 和 非公平锁。

6.8.1 ReentrantLock 非公平锁的抢占流程

ReentrantLock 为非公平所实现了一个内部的同步器——NonfairSync ,其显式锁获取方法 lock() 源码如下:

1
2
3
4
5
6
7
8
9
10
//非公平抢占
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);
}
}
}

非公平性就体现在这里:如果占用锁的线程刚释放锁,state 为 0,而队列中排队等待锁的线程还未唤醒,新来的线程就直接抢占了该锁,那么就插队了。举个例子:假设 A、B 线程在排队等锁,但是此时不在队列中的 C 直接进行 CAS 操作成功了,拿到锁开开心心返回了,那么 A、 B 只能乖乖看着。

6.8.4 ReentrantLock 公平锁的抢占流程

ReentrantLock 为公平所实现了一个内部的同步器——FairSync ,其显式锁获取方法 lock() 源码如下:

1
2
3
4
5
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}

其核心思想是通过 AQS 模板方法 acquire 进行队列入队操作。

6.8.5 AQS模板方法 acquire(arg)

自己调整的章节,本来这节在前面,但是放前面又看不懂,用意也不太明确。

acquire(arg) 方法是 AQS 提供的利用独占的方式获取资源的方法,源码实现如下:

1
2
3
4
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

它的含义是:如果通过 tryAcquire(arg)方法尝试成功,则直接返回,表示已经抢到锁;否则,将线程加入等待队列。

6.8.6 AQS模板方法 tryAcquire(arg)

在 ReentrantLock 中,在公平锁状态和非公平锁状态下, tryAcquire 的实现是不一样的。

  • 公平锁状态下会判断是否是队头,是队头就允许CAS获取锁;如果不是就判断是否是重入,重入允许进入;否则就返回了false了(自己看代码总结的);
  • 非公平状态下还是会直接 CAS 抢锁了,不管队头这些了,这也是非公平锁的行为体现
谢谢你的鼓励