这是我参与 8 月更文挑战的第 5 天
往期推荐
- 深入了解Java异常
- Java基础:Object类
- 这些Object类的常见面试题,你了解吗?
- Spring常用API:Spring类及相关面试点
- Java并发编程:线程
- 浅谈 “线程池”
- Java并发-常见的阻塞队列详解
- Java中的锁 | 8月更文挑战
一、AQS简介
AQS:AbstractQueuedSynchronizer,即抽象的队列同步器,是通过维护一个共享资源状态(Volatile Int State)和一个先进先出(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。
基于AQS构建同步器:
-
ReentrantLock
-
Semaphore
-
CountDownLatch
-
ReentrantReadWriteLock
-
SynchronusQueue
-
FutureTask 优势:
-
AQS 解决了在实现同步器时涉及的大量细节问题,例如自定义标准同步状态、FIFO 同步队列。
-
基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
二、AQS的核心知识
2.1 AQS的核心思想
AQS(AbstractQueuedSynchronizer)核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点 Node 来实现锁的分配。 AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过procted类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2 AQS的数据结构
CLH队列图
- Sync queue: 同步队列,是一个双向列表。包括head节点和tail节点。head节点主要用作后续的调度。
- Condition queue: 非必须,单向列表。当程序中存在cindition的时候才会存在此列表。
2.3 AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
同步器可重写的方法如下表所示。
| 方法名称 | 描述 |
|---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行 CAS 设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于 0 的值,表示获取成功,反之,获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 |
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
同步器提供的模板方法分为 3 类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况
2.4 AQS对资源的共享方式
AQS定义两种资源共享方式
-
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
-
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读写。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。
2.5 AQS的类结构
2.5.1 类的继承关系
AbstractQueuedSynchronizer继承自AbstractOwnableSynchronizer抽象类,并且实现了Serializable接口,可以进行序列化。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable
其中AbstractOwnableSynchronizer抽象类的源码如下:
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
// 版本序列号
private static final long serialVersionUID = 3737899427754241961L;
// 构造方法
protected AbstractOwnableSynchronizer() { }
// 独占模式下的线程
private transient Thread exclusiveOwnerThread;
// 设置独占线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
// 获取独占线程
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
AbstractOwnableSynchronizer抽象类中,可以设置独占资源线程和获取独占资源线程。
分别为setExclusiveOwnerThread与getExclusiveOwnerThread方法,这两个方法会被子类调用。
2.5.2 类的内部类 - Node类
static final class Node {
// 模式,分为共享与独占
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 结点状态
// CANCELLED,值为1,表示当前的线程被取消
// SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
// CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
// PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
// 值为0,表示当前节点在sync队列中,等待着获取锁
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 结点状态
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() { // Used to establish initial head or SHARED marker
}
// 构造方法
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
// 构造方法
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
每个线程被阻塞的线程都会被封装成一个Node结点,放入队列。每个节点包含了一个Thread类型的引用,并且每个节点都存在一个状态,具体状态如下。
- 值为0,表示当前节点在sync queue中,等待着获取锁。
CANCELLED,值为1,表示当前的线程被取消。SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,需要进行unpark操作。CONDITION,值为-2,表示当前节点在等待condition,也就是在condition queue中。PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行。
2.5.3 类的属性
属性中包含了头结点head,尾结点tail,状态state、自旋时间spinForTimeoutThreshold,还有AbstractQueuedSynchronizer抽象的属性在内存中的偏移地址,通过该偏移地址,可以获取和设置该属性的值,同时还包括一个静态初始化块,用于加载内存偏移地址。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 版本号
private static final long serialVersionUID = 7373984972572414691L;
// 头结点
private transient volatile Node head;
// 尾结点
private transient volatile Node tail;
// 状态
private volatile int state;
// 自旋时间
static final long spinForTimeoutThreshold = 1000L;
// Unsafe类实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// state内存偏移地址
private static final long stateOffset;
// head内存偏移地址
private static final long headOffset;
// state内存偏移地址
private static final long tailOffset;
// tail内存偏移地址
private static final long waitStatusOffset;
// next内存偏移地址
private static final long nextOffset;
// 静态初始化块
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
}
三、独占模式和共享模式
3.1 AQS独占模式
所谓独占模式,即只允许一个线程获取同步状态,当这个线程还没有释放同步状态时,其他线程是获取不了的,只能加入到同步队列,进行等待。
独占模式两个功能
- 1、获取同步资源的功能。当多个线程同时竞争同步资源的时候,只有一个线程能获取到同步资源,其他未获取到同步资源的线程必须在当前位置等待。
- 2、释放同步资源的功能。获取同步资源的线程用完同步资源后,释放这个同步资源,并唤醒正在等待同步资源的一个或多个线程。
3.1.1 独占模式获取资源(acquire方法)
独占模式下,获取资源的使用权主要是通过acquire()方法实现的,acquire()方法代码如下:
/**
* 以独占的方式获取同步资源,此方法不响应中断
*/
public final void acquire(int arg) {
//1、调用tryAcquire()方法尝试获取同步资源
//如果tryAcquire()方法返回true,那么acquore()方法执行结束
//tryAcquire()方法一般在AQS自雷中实现
//2、如果tryAcquire()方法返回false
//就调用addWaiter()方法将当前调用acquire()方法的线程入队
//3、调用acquireQueued()方法在等待队列中获取同步资源
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其执行流程可以分为以下几步:
1. 执行tryAcquire()方法
调用tryAcquire()方法尝试获取同步资源的使用权限。如果获取成功, tryAcquire()方法就会返回true,整个if条件将会返回false,因此acquire()执行结束。如果tryAcquire()方法返回false,就说明当前环境中存在竞争同步资源的线程,因此造成当前的线程获取同步资源的使用权限失败。当tryAcquire()方法返回false时,则执行步骤2。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
2. 执行addWaiter()方法
/**
* 将未获取到共享资源的线程添加到队列的尾结点
*方法参数 Node.EXCLUSIVE 表示当前是独占模式
*/
private Node addWaiter (Node mode){
//用当前线程创建一个 Node 结点
Node node = new Node(Thread.currentThread(),mode);
// 以下代码的执行逻辑是∶
// 首先获取原队列的尾结点
// 将当前结点加入队列尾部,如果入队成功,就返回新结点 node
// 如果入队失败,就采用自旋加入结点直至入队成功返回该结点
Node pred = tail;
// 如果当前队尾非空
if (pred != null){
//将 node 结点的 prev 引用指向队尾结点
node.prev = pred;
// 如果通过 CAS 入队尾成功
// 即 node 结点成为新的对尾结点
if (compareAndSetTail(pred,node)){
// 原队尾 pred 结点的 next 引用指向 node
pred.next = node;
// 返回 node
return node;
}
}
// 如果队尾为空
//或者通过CAS 进入队尾失败,即当前环境存在多个线程竞争入队
// 通过 enq()方法自旋
enq (node);
// 返回 node 结点
return node;
}
addWaiter()方法通过CAS(Compare And Swap,比较再交换)方式使线程进入队尾。CAS有3个操作数,分别是内存值V、旧的期望值A和将要修改的新值B。当且仅当旧的期望值A和内存值V相等时,将内存值V修改为新值B。在addWaiter()方法中通过CAS方式入队,当且仅当队尾结点没有被修改时,当前结点才可以入队成功,否则执行enq()方法
/**
* 自旋方式使 node 结点进入队尾
*/
private Node enq(final Node node){
// 自旋
for (;;){
// 队尾结点
Node t = tail;
// 如果队尾结点为空,即队列为空
if (t == null){
// 创建虚拟头结点
// 通过 CAS 设置队列头结点
if (compareAndSetHead(new Node()))
// 此时队列只有一个结点
// 即头结点等于尾结点
tail = head;
} else (
// 如果队列尾结点非空
// 设置node 结点的 prev 引用指向t
node.prev = t;
// 通过CAS 设置新的队尾结点为 node 结点
if (compareAndSetTail(t,node)){
// 原队尾结点的 next 引用指向 node
// node 结点就是新的尾结点 tail
t.next = node;
// 自旋结束,返回原尾结点
return t;
}
}
}
}
3. 执行acquireQueued()方法
当线程进入同步队列后,会在同步队列中等待同步资源的释放。当线程争取到同步资源的使用权后,线程将会从同步队列中出队。
final boolean acquireQueued(final Node node, int arg) {
//是否已获取锁的标志,默认为true 即为尚未
boolean failed = true;
try {
//等待中是否被中断过的标记
boolean interrupted = false;
for (;;) {
//获取前节点
final Node p = node.predecessor();
//如果当前节点已经成为头结点,尝试获取锁(tryAcquire)成功,然后返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire根据对当前节点的前一个节点的状态进行判断,对当前节点做出不同的操作
//parkAndCheckInterrupt让线程进入等待状态,并检查当前线程是否被可以被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//将当前节点设置为取消状态;取消状态设置为1
if (failed)
cancelAcquire(node);
}
}
- 3.1
acquireQueued()方法调用predecessor()方法获取结点的前驱结点。
/**
* 获取当前结点的前驱结点,会抛出 Nul1PointerException 异常
*/
final Node predecessor()throws NullPointerException {
// 指向当前结点前驱结点的引用
Node p = prev;
// 如果前驱结点为空,就抛出 NullPointerException
if (p == null)
throw new NullPointerException();
else
// 返回前驱结点
return p;
}
- 3.2
acquireQueued()调用predecessor()方法获取当前结点的前驱结点后,如果当前结点的前驱结点为头结点,就调用tryAcquire()方法尝试获取资源。如果tryAcquire()方法返回true,即获取资源成功,就通过setHead()方法将当前结点设置为头结点。
private void setHead(Node node) {
head = rnode;
node.thread = null;
node.prev = null;
}
- 3.3
acquireQueued()调用shouldParkAfterFailedAcquire()方法
// 当获取(资源)失败后,检查并且更新结点状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱结点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 状态为SIGNAL,为-1
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
// 可以进行park操作
return true;
if (ws > 0) { // 表示状态为CANCELLED,为1
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 找到pred结点前面最近的一个状态不为CANCELLED的结点
// 赋值pred结点的next域
pred.next = node;
} else { // 为PROPAGATE -3 或者是0 表示无状态,(为CONDITION -2时,表示此节点在condition queue中)
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 比较并设置前驱结点的状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 不能进行park操作
return false;
}
- 3.4
shouldParkAfterFailedAcquire()方法返回false,就会造成线程进入acquireQueued()方法自旋;如果shouldParkAfterFailedAcquire()方法返回true,就会执行parkAndCheckInterrupt()方法。
// 进行park操作并且返回该线程是否被中断
private final boolean parkAndCheckInterrupt() {
// 在许可可用之前禁用当前线程,并且设置了blocker
LockSupport.park(this);
return Thread.interrupted(); // 当前线程是否已被中断,并清除中断标记位
}
4. 执行selfInterrupt()方法
如果线程在期间发生中断,就维护线程中断的状态,但并不响应中断。
/**
* 设置线程中断,并不对中断做出响应
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
独占模式获取资源的执行流程:
3.1.2 独占模式释放资源(release方法)
独占模式下,释放资源的使用权是通过release()方法实现的,release()方法代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
其执行流程可以分为以下几步:
1. 执行tryRelease()方法
调用tryRelease()方法尝试释放同步资源的使用权限。若释放成功,则tryRelease()方法将会返回true,然后执行步骤2。若tryRelease()方法返回false,则释放资源失败。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
2. 执行unparkSuccessor()方法
unparkSuccessor()方法唤醒等待队列中的后继结点,使之可以再次竞争同步资源。
/**
* 唤醒同步队列的后继结点
*/
private void unparkSuccessor(Node node) {
/*结点的状态
* 如果小于零,则肯定不是 CANCELLED状态
*/
int ws = node.waitStatus;
if (ws < 0)
//通过 CAS 方式将结点状态修改为 0
compareAndSetWaitStatus(node, ws, 0);
/*
* node 结点的后继结点
*/
Node s = node.next;
//如果后继结点非空,且状态大于0,即 CANCELLED状态
//说明后继结点对应的线程取消对资源的等待
if (s == null II s.waitStatus > 0){
// 将后继结点置为空
s = null;
// 从同步队列尾结点开始向前遍历,
// 找到同步队列中 node 结点后第一个等待唤醒的结点
// 如果遍历到的结点 t 非空且不等于当前结点 node,
// 则校验结点 t 的状态
for (Node t = tail; t != null && t != node; t = t.prev)
// 如果结点的状态小于等于0
if (t.waitStatus <= 0)
// 则将 s 指向 t
s = t;
}
// 如果结点s 非空
if (s != null)
// 唤醒结点s 对应的线程
LockSupport.unpark(s.thread);
}
unparkSuccessor()方法执行流程如下:
(1)将node结点的状态设置为0。
(2)寻找到下一个非取消状态的结点s。
(3)如果结点s不为null,则调用LockSupport.unpark(s.thread)方法唤醒结点s对应的线程。
(4)unparkSuccessor()方法唤醒线程的顺序即线程添加到同步队列的顺序。
3.2 AQS共享模式
3.2.1 共享模式获取资源(acquireShared方法)
在共享模式下,获取资源的使用权主要是通过acquireShared()方法实现的。acquireShared()方法代码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
其执行流程可以分为以下几步:
1. 执行tryAcquireShared()方法
AbstractQueuedSynchronizer类并未实现tryAcquireShared()方法,而是交由其子类实现。如果tryAcquireShared()方法返回小于0,即线程获取共享资源失败,就通过doAcquireShared()方法使线程进入同步队列。
- 负值代表获取失败;
- 0代表获取成功,但没有剩余资源;
- 正数表示获取成功,还有剩余资源,其他线程还可以去获取。
// 共享模式下尝试获取资源,此方法需要由子类覆盖
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
2. 执行doAcquireShared()方法
共享模式调用的是addWaiter(Node.SHARED)方法将线程加入队列的尾部,表明该结点处于共享模式。获取资源的线程调用setHeadAndPropagate(node, r)方法。
private void doAcquireShared(int arg) {
//加入队列尾部
final Node node = addWaiter(Node.SHARED);
//是否成功标志
boolean failed = true;
try {
//等待过程中是否被中断过的标志
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获取前驱节点
if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}
//判断状态,队列寻找一个适合位置,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
3.2.2 共享模式释放资源( releaseShared方法)
共享模式下,释放资源的使用权是通过releaseShared()方法实现的,releaseShared()方法代码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
1. 执行tryReleaseShared()方法
tryReleaseShared()方法执行成功,就会执行doReleaseShared()方法唤醒后继结点。
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
2. 执行doReleaseShared()方法
唤醒后继节点线程
- 当state为正数,去获取剩余共享资源;
- 当state=0时去获取共享资源。
private void doReleaseShared() {
/*
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {// 这个head!=tail 说明阻塞队列中至少2个节点 不然也没必要去传播唤醒 如果就自己一个节点 就算资源条件满足 还换个谁呢?
int ws = h.waitStatus;// head 节点状态SIGNAL
if (ws == Node.SIGNAL) {// 如果head状态是
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);//是和独占锁释放用的同样的方法 唤醒的是下一个节点 上一篇有分析到
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; //这边设置为-3 是为了唤醒的传播 也就是满足上一个方法有判断waitStatus 小于0
}
if (h == head)
break;
}
}
四 参考资料
Doug Lea:《Java并发编程实战》
方腾飞:《Java并发编程的艺术》