AQS 介绍

AQS 介绍

AQS 的全称为 AbstractQueuedSynchronizer,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。

AbstractQueuedSynchronizer
java.util.concurrent.locks

AQS 就是一个抽象类,主要用来构建锁和同步器。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}

AQS 为构建锁和同步器提供了一些通用功能的实现。因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue 等等皆是基于 AQS 的。

ReentrantLock
Semaphore
ReentrantReadWriteLock
SynchronousQueue

AQS 原理

AQS 原理

在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。

AQS 快速了解

AQS 快速了解

在真正讲解 AQS 源码之前,需要对 AQS 有一个整体层面的认识。这里会先通过几个问题,从整体层面上认识 AQS,了解 AQS 在整个 Java 并发中所位于的层面,之后在学习 AQS 源码的过程中,才能更加了解同步器和 AQS 之间的关系。

AQS 的作用是什么?

AQS 的作用是什么?

AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 可重入锁(ReentrantLock)、信号量(Semaphore)和 倒计时器(CountDownLatch)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。

ReentrantLock
Semaphore
CountDownLatch

简单来说,AQS 是一个抽象类,为同步器提供了通用的 执行框架。它定义了 资源获取和释放的通用流程,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 基础“底座”,而同步器则是基于 AQS 实现的 具体“应用”。

AQS 为什么使用 CLH 锁队列的变体?

AQS 为什么使用 CLH 锁队列的变体?

CLH 锁是一种基于 自旋锁 的优化实现。

先说一下自旋锁存在的问题:自旋锁通过线程不断对一个原子变量执行 compareAndSet(简称 CAS)操作来尝试获取锁。在高并发场景下,多个线程会同时竞争同一个原子变量,容易造成某个线程的 CAS 操作长时间失败,从而导致 “饥饿”问题(某些线程可能永远无法获取锁)。

compareAndSet
CAS
CAS

CLH 锁通过引入一个队列来组织并发竞争的线程,对自旋锁进行了改进:

每个线程会作为一个节点加入到队列中,并通过自旋监控前一个线程节点的状态,而不是直接竞争共享变量。

线程按顺序排队,确保公平性,从而避免了 “饥饿” 问题。

AQS(AbstractQueuedSynchronizer)在 CLH 锁的基础上进一步优化,形成了其内部的 CLH 队列变体。主要改进点有以下两方面:

自旋 + 阻塞: CLH 锁使用纯自旋方式等待锁的释放,但大量的自旋操作会占用过多的 CPU 资源。AQS 引入了 自旋 + 阻塞 的混合机制: 如果线程获取锁失败,会先短暂自旋尝试获取锁;如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少 CPU 的浪费。

如果线程获取锁失败,会先短暂自旋尝试获取锁;

如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少 CPU 的浪费。

单向队列改为双向队列:CLH 锁使用单向队列,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS 将队列改为 双向队列,新增了 next 指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。

next

AQS 的性能比较好,原因是什么?

AQS 的性能比较好,原因是什么?

因为 AQS 内部大量使用了 CAS 操作。

CAS

AQS 内部通过队列来存储等待的线程节点。由于队列是共享资源,在多线程场景下,需要保证队列的同步访问。

AQS 内部通过 CAS 操作来控制队列的同步访问,CAS 操作主要用于控制 队列初始化、 线程节点入队 两个操作的并发安全。虽然利用 CAS 控制并发安全可以保证比较好的性能,但同时会带来比较高的 编码复杂度。

CAS
CAS
队列初始化
线程节点入队
CAS

AQS 中为什么 Node 节点需要不同的状态?

AQS 中为什么 Node 节点需要不同的状态?

AQS 中的 waitStatus 状态类似于 状态机,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。

waitStatus

状态 0:新节点加入队列之后,初始状态为 0。

状态 0:新节点加入队列之后,初始状态为 0。

0
0

状态 SIGNAL:当有新的节点加入队列,此时新节点的前继节点状态就会由 0 更新为 SIGNAL,表示前继节点释放锁之后,需要对新节点进行唤醒操作。如果唤醒 SIGNAL 状态节点的后续节点,就会将 SIGNAL 状态更新为 0。即通过清除 SIGNAL 状态,表示已经执行了唤醒操作。

状态 SIGNAL:当有新的节点加入队列,此时新节点的前继节点状态就会由 0 更新为 SIGNAL,表示前继节点释放锁之后,需要对新节点进行唤醒操作。如果唤醒 SIGNAL 状态节点的后续节点,就会将 SIGNAL 状态更新为 0。即通过清除 SIGNAL 状态,表示已经执行了唤醒操作。

SIGNAL
0
SIGNAL
SIGNAL
SIGNAL
0
SIGNAL

状态 CANCELLED:如果一个节点在队列中等待获取锁锁时,因为某种原因失败了,该节点的状态就会变为 CANCELLED,表明取消获取锁,这种状态的节点是异常的,无法被唤醒,也无法唤醒后继节点。

状态 CANCELLED:如果一个节点在队列中等待获取锁锁时,因为某种原因失败了,该节点的状态就会变为 CANCELLED,表明取消获取锁,这种状态的节点是异常的,无法被唤醒,也无法唤醒后继节点。

CANCELLED
CANCELLED

AQS 核心思想

AQS 核心思想

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁(Craig, Landin, and Hagersten locks) 进一步优化实现的。

CLH 锁 对自旋锁进行了改进,是基于单链表的自旋锁。在多线程场景下,会将请求获取锁的线程组织成一个单向队列,每个等待的线程会通过自旋访问前一个线程节点的状态,前一个节点释放锁之后,当前节点才可以获取锁。CLH 锁 的队列结构如下图所示。

CLH 锁的队列结构

AQS 中使用的 等待队列 是 CLH 锁队列的变体(接下来简称为 CLH 变体队列)。

AQS 的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH 变体队列和原本的 CLH 锁队列的区别主要有两点:

由 自旋 优化为 自旋 + 阻塞:自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。

由 单向队列 优化为 双向队列:在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 next 指针,成为了双向队列。

next

AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点(Node)来实现锁的分配。在 CLH 变体队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。

AQS 中的 CLH 变体队列结构如下图所示:

CLH 变体队列结构

关于 AQS 核心数据结构-CLH 锁的详细解读,强烈推荐阅读 Java AQS 核心数据结构-CLH 锁 - Qunar 技术沙龙 这篇文章。

Java AQS 核心数据结构-CLH 锁 - Qunar 技术沙龙

AQS(AbstractQueuedSynchronizer)的核心原理图:

AbstractQueuedSynchronizer

CLH 变体队列

AQS 使用 int 成员变量 state 表示同步状态,通过内置的 FIFO 线程等待/等待队列 来完成获取资源线程的排队工作。

state

state 变量由 volatile 修饰,用于展示当前临界资源的获取情况。这里 volatile 的作用不仅仅是保证可见性,更重要的是通过 happens-before 规则(volatile 变量的写操作先行发生于后续的读操作)防止编译器和处理器对指令进行重排序,从而保证锁语义的正确性。

state
volatile
volatile
// 共享变量,使用volatile修饰,保证线程可见性并防止指令重排序
private volatile int state;
// 共享变量,使用volatile修饰,保证线程可见性并防止指令重排序
private volatile int state;

另外,状态信息 state 可以通过 protected 类型的 getState()、setState() 和 compareAndSetState() 进行操作。并且,这几个方法都是 final 修饰的,在子类中无法被重写。

state
protected
getState()
setState()
compareAndSetState()
final
//返回同步状态的当前值
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);
}
//返回同步状态的当前值
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);
}

以可重入的互斥锁 ReentrantLock 为例,它的内部维护了一个 state 变量,用来表示锁的占用状态。state 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 变体队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。

ReentrantLock
state
state
lock()
tryAcquire()
state
state
state

线程 A 尝试获取锁的过程如下图所示(图源从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队):

从 ReentrantLock 的实现看 AQS 的原理及应用 - 美团技术团队

AQS 独占模式获取锁

再以倒计时器 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程开始执行任务,每执行完一个子线程,就调用一次 countDown() 方法。该方法会尝试使用 CAS(Compare and Swap) 操作,让 state 的值减少 1。当所有的子线程都执行完毕后(即 state 的值变为 0),CountDownLatch 会调用 unpark() 方法,唤醒主线程。这时,主线程就可以从 await() 方法(CountDownLatch 中的 await() 方法而非 AQS 中的)返回,继续执行后续的操作。

CountDownLatch
state
countDown()
state
state
CountDownLatch
unpark()
await()
CountDownLatch
await()

Node 节点 waitStatus 状态含义

Node 节点 waitStatus 状态含义

AQS 中的 waitStatus 状态类似于 状态机,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转。

waitStatus

Node 节点状态值含义CANCELLED1表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。SIGNAL-1表示后继节点需要当前节点唤醒。在当前线程节点释放锁之后,需要对后继节点进行唤醒。CONDITION-2表示节点在等待 Condition。当其他线程调用了 Condition 的 signal() 方法后,节点会从等待队列转移到同步队列中等待获取资源。PROPAGATE-3用于共享模式。在共享模式下,可能会出现线程在队列中无法被唤醒的情况,因此引入了 PROPAGATE 状态来解决这个问题。0加入队列的新节点的初始状态。

Node 节点状态值含义

Node 节点状态

含义

CANCELLED1表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。

CANCELLED

CANCELLED

1

表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。

SIGNAL-1表示后继节点需要当前节点唤醒。在当前线程节点释放锁之后,需要对后继节点进行唤醒。

SIGNAL

SIGNAL

-1

表示后继节点需要当前节点唤醒。在当前线程节点释放锁之后,需要对后继节点进行唤醒。

CONDITION-2表示节点在等待 Condition。当其他线程调用了 Condition 的 signal() 方法后,节点会从等待队列转移到同步队列中等待获取资源。

CONDITION

CONDITION

-2

表示节点在等待 Condition。当其他线程调用了 Condition 的 signal() 方法后,节点会从等待队列转移到同步队列中等待获取资源。

signal()

PROPAGATE-3用于共享模式。在共享模式下,可能会出现线程在队列中无法被唤醒的情况,因此引入了 PROPAGATE 状态来解决这个问题。

PROPAGATE

PROPAGATE

-3

用于共享模式。在共享模式下,可能会出现线程在队列中无法被唤醒的情况,因此引入了 PROPAGATE 状态来解决这个问题。

PROPAGATE

0加入队列的新节点的初始状态。

0

加入队列的新节点的初始状态。

在 AQS 的源码中,经常使用 > 0、 < 0 来对 waitStatus 进行判断。

> 0
< 0
waitStatus

如果 waitStatus > 0,表明节点的状态已经取消等待获取资源。

waitStatus > 0

如果 waitStatus < 0,表明节点的状态处于正常的状态,即没有取消等待。

waitStatus < 0

其中 SIGNAL 状态是最重要的,节点状态流转以及对应操作如下:

SIGNAL

状态流转对应操作0新节点入队时,初始状态为 0 。0 -> SIGNAL新节点入队时,它的前继节点状态会由 0 更新为 SIGNAL 。SIGNAL 状态表明该节点的后续节点需要被唤醒。SIGNAL -> 0在唤醒后继节点时,需要清除当前节点的状态。通常发生在 head 节点,比如 head 节点的状态由 SIGNAL 更新为 0 ,表示已经对 head 节点的后继节点唤醒了。0 -> PROPAGATEAQS 内部引入了 PROPAGATE 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到)

状态流转对应操作

状态流转

对应操作

0新节点入队时,初始状态为 0 。

0

0

新节点入队时,初始状态为 0 。

0

0 -> SIGNAL新节点入队时,它的前继节点状态会由 0 更新为 SIGNAL 。SIGNAL 状态表明该节点的后续节点需要被唤醒。

0 -> SIGNAL

0 -> SIGNAL

新节点入队时,它的前继节点状态会由 0 更新为 SIGNAL 。SIGNAL 状态表明该节点的后续节点需要被唤醒。

0
SIGNAL
SIGNAL

SIGNAL -> 0在唤醒后继节点时,需要清除当前节点的状态。通常发生在 head 节点,比如 head 节点的状态由 SIGNAL 更新为 0 ,表示已经对 head 节点的后继节点唤醒了。

SIGNAL -> 0

SIGNAL -> 0

在唤醒后继节点时,需要清除当前节点的状态。通常发生在 head 节点,比如 head 节点的状态由 SIGNAL 更新为 0 ,表示已经对 head 节点的后继节点唤醒了。

head
head
SIGNAL
0
head

0 -> PROPAGATEAQS 内部引入了 PROPAGATE 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到)

0 -> PROPAGATE

0 -> PROPAGATE

AQS 内部引入了 PROPAGATE 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到)

PROPAGATE

自定义同步器

自定义同步器

基于 AQS 可以实现自定义的同步器, AQS 提供了 5 个模板方法(模板方法模式)。如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

自定义的同步器继承 AbstractQueuedSynchronizer。

AbstractQueuedSynchronizer

重写 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()
//独占方式。尝试获取资源,成功则返回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 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。

protected

篇幅问题,这里就不详细介绍模板方法模式了,不太了解的小伙伴可以看看这篇文章:用 Java8 改造后的模板方法模式真的是 yyds!。

用 Java8 改造后的模板方法模式真的是 yyds!

除了上面提到的钩子方法之外,AQS 类中的其他方法都是 final,所以无法被其他类重写。

final

AQS 资源共享方式

AQS 资源共享方式

AQS 定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如 ReentrantLock)和 Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。

Exclusive
ReentrantLock
Share
Semaphore
CountDownLatch

一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。

tryAcquire-tryRelease
tryAcquireShared-tryReleaseShared
ReentrantReadWriteLock

独占模式与共享模式的深入对比

独占模式与共享模式的深入对比

上面简要介绍了 AQS 的两种资源共享方式,下面从多个维度对独占模式和共享模式进行系统对比,帮助更深入地理解二者的差异。

特性对比

特性对比

对比维度独占模式(Exclusive)共享模式(Share)并发度同一时刻只有一个线程能获取到资源同一时刻可以有多个线程同时获取到资源获取资源入口acquire(int arg)acquireShared(int arg)释放资源入口release(int arg)releaseShared(int arg)需要重写的模板方法tryAcquire(int) / tryRelease(int)tryAcquireShared(int) / tryReleaseShared(int)tryXxx 返回值boolean,true 表示获取/释放成功int(获取时),负数表示失败,0 表示成功但无剩余资源,正数表示成功且有剩余资源;boolean(释放时)唤醒后继节点释放资源时唤醒一个后继节点获取资源成功后,如果还有剩余资源,会继续唤醒后续节点(传播唤醒)Node 类型标识Node.EXCLUSIVE(null)Node.SHARED(一个静态的 Node 实例)典型实现ReentrantLock、ReentrantReadWriteLock 的写锁Semaphore、CountDownLatch、ReentrantReadWriteLock 的读锁

对比维度独占模式(Exclusive)共享模式(Share)

对比维度

独占模式(Exclusive)

共享模式(Share)

并发度同一时刻只有一个线程能获取到资源同一时刻可以有多个线程同时获取到资源

并发度

同一时刻只有一个线程能获取到资源

同一时刻可以有多个线程同时获取到资源

获取资源入口acquire(int arg)acquireShared(int arg)

获取资源入口

acquire(int arg)

acquire(int arg)

acquireShared(int arg)

acquireShared(int arg)

释放资源入口release(int arg)releaseShared(int arg)

释放资源入口

release(int arg)

release(int arg)

releaseShared(int arg)

releaseShared(int arg)

需要重写的模板方法tryAcquire(int) / tryRelease(int)tryAcquireShared(int) / tryReleaseShared(int)

需要重写的模板方法

tryAcquire(int) / tryRelease(int)

tryAcquire(int)
tryRelease(int)

tryAcquireShared(int) / tryReleaseShared(int)

tryAcquireShared(int)
tryReleaseShared(int)

tryXxx 返回值boolean,true 表示获取/释放成功int(获取时),负数表示失败,0 表示成功但无剩余资源,正数表示成功且有剩余资源;boolean(释放时)

tryXxx 返回值

boolean,true 表示获取/释放成功

boolean
true

int(获取时),负数表示失败,0 表示成功但无剩余资源,正数表示成功且有剩余资源;boolean(释放时)

int
boolean

唤醒后继节点释放资源时唤醒一个后继节点获取资源成功后,如果还有剩余资源,会继续唤醒后续节点(传播唤醒)

唤醒后继节点

释放资源时唤醒一个后继节点

获取资源成功后,如果还有剩余资源,会继续唤醒后续节点(传播唤醒)

Node 类型标识Node.EXCLUSIVE(null)Node.SHARED(一个静态的 Node 实例)

Node 类型标识

Node.EXCLUSIVE(null)

Node.EXCLUSIVE
null

Node.SHARED(一个静态的 Node 实例)

Node.SHARED
Node

典型实现ReentrantLock、ReentrantReadWriteLock 的写锁Semaphore、CountDownLatch、ReentrantReadWriteLock 的读锁

典型实现

ReentrantLock、ReentrantReadWriteLock 的写锁

ReentrantLock
ReentrantReadWriteLock

Semaphore、CountDownLatch、ReentrantReadWriteLock 的读锁

Semaphore
CountDownLatch
ReentrantReadWriteLock

state 在不同同步器中的语义

state 在不同同步器中的语义

state

AQS 中的 state 是一个通用的同步状态变量,不同的同步器赋予它不同的含义:

state

同步器模式state 的语义ReentrantLock独占表示锁的重入次数。state == 0 表示锁空闲;state > 0 表示锁被持有,值为重入次数ReentrantReadWriteLock独占 + 共享高 16 位表示读锁的持有数量(共享),低 16 位表示写锁的重入次数(独占)Semaphore共享表示可用许可证的数量。每次 acquire() 减少,release() 增加CountDownLatch共享表示需要等待的计数。每次 countDown() 减 1,到 0 时唤醒所有等待线程

同步器模式state 的语义

同步器

模式

state 的语义

state

ReentrantLock独占表示锁的重入次数。state == 0 表示锁空闲;state > 0 表示锁被持有,值为重入次数

ReentrantLock

ReentrantLock

独占

表示锁的重入次数。state == 0 表示锁空闲;state > 0 表示锁被持有,值为重入次数

state == 0
state > 0

ReentrantReadWriteLock独占 + 共享高 16 位表示读锁的持有数量(共享),低 16 位表示写锁的重入次数(独占)

ReentrantReadWriteLock

ReentrantReadWriteLock

独占 + 共享

高 16 位表示读锁的持有数量(共享),低 16 位表示写锁的重入次数(独占)

Semaphore共享表示可用许可证的数量。每次 acquire() 减少,release() 增加

Semaphore

Semaphore

共享

表示可用许可证的数量。每次 acquire() 减少,release() 增加

acquire()
release()

CountDownLatch共享表示需要等待的计数。每次 countDown() 减 1,到 0 时唤醒所有等待线程

CountDownLatch

CountDownLatch

共享

表示需要等待的计数。每次 countDown() 减 1,到 0 时唤醒所有等待线程

countDown()

下面通过一个代码示例来直观感受独占模式和共享模式在使用上的区别:

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

public class ExclusiveVsSharedDemo {
    public static void main(String[] args) {
        // 独占模式:同一时刻只有 1 个线程能进入临界区
        ReentrantLock lock = new ReentrantLock();

        // 共享模式:同一时刻最多 3 个线程能进入临界区
        Semaphore semaphore = new Semaphore(3);

        // 独占模式示例
        Runnable exclusiveTask = () -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()
                        + " 获取到独占锁,正在执行...");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        };

        // 共享模式示例
        Runnable sharedTask = () -> {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()
                        + " 获取到许可证,正在执行...");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                semaphore.release();
            }
        };

        System.out.println("=== 独占模式(ReentrantLock)===");
        for (int i = 0; i < 5; i++) {
            new Thread(exclusiveTask, "独占线程-" + i).start();
        }

        try { Thread.sleep(3000); } catch (InterruptedException e) { }

        System.out.println("\n=== 共享模式(Semaphore)===");
        for (int i = 0; i < 5; i++) {
            new Thread(sharedTask, "共享线程-" + i).start();
        }
    }
}
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

public class ExclusiveVsSharedDemo {
    public static void main(String[] args) {
        // 独占模式:同一时刻只有 1 个线程能进入临界区
        ReentrantLock lock = new ReentrantLock();

        // 共享模式:同一时刻最多 3 个线程能进入临界区
        Semaphore semaphore = new Semaphore(3);

        // 独占模式示例
        Runnable exclusiveTask = () -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()
                        + " 获取到独占锁,正在执行...");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        };

        // 共享模式示例
        Runnable sharedTask = () -> {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()
                        + " 获取到许可证,正在执行...");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                semaphore.release();
            }
        };

        System.out.println("=== 独占模式(ReentrantLock)===");
        for (int i = 0; i < 5; i++) {
            new Thread(exclusiveTask, "独占线程-" + i).start();
        }

        try { Thread.sleep(3000); } catch (InterruptedException e) { }

        System.out.println("\n=== 共享模式(Semaphore)===");
        for (int i = 0; i < 5; i++) {
            new Thread(sharedTask, "共享线程-" + i).start();
        }
    }
}

运行上面的代码可以观察到:独占模式下 5 个线程严格按顺序一个一个执行,而共享模式下最多有 3 个线程同时执行。

AQS 资源获取源码分析(独占模式)

AQS 资源获取源码分析(独占模式)

AQS 中以独占模式获取资源的入口方法是 acquire(),如下:

acquire()
// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在 acquire() 中,线程会先尝试获取共享资源;如果获取失败,会将线程封装为 Node 节点加入到 AQS 的等待队列中;加入队列之后,会让等待队列中的线程尝试获取资源,并且会对线程进行阻塞操作。分别对应以下三个方法:

acquire()

tryAcquire():尝试获取锁(模板方法),AQS 不提供具体实现,由子类实现。

tryAcquire()
AQS

addWaiter():如果获取锁失败,会将当前线程封装为 Node 节点加入到 AQS 的 CLH 变体队列中等待获取锁。

addWaiter()

acquireQueued():对线程进行阻塞,并调用 tryAcquire() 方法让队列中的线程尝试获取锁。

acquireQueued()
tryAcquire()

tryAcquire() 分析

tryAcquire() 分析

tryAcquire()

AQS 中对应的 tryAcquire() 模板方法如下:

tryAcquire()
// AQS
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
// AQS
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

tryAcquire() 方法是 AQS 提供的模板方法,不提供默认实现。

tryAcquire()

因此,这里分析 tryAcquire() 方法时,以 ReentrantLock 的非公平锁(独占锁)为例进行分析,ReentrantLock 内部实现的 tryAcquire() 会调用到下边的 nonfairTryAcquire():

tryAcquire()
ReentrantLock
ReentrantLock
tryAcquire()
nonfairTryAcquire()
// ReentrantLock
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 1、获取 AQS 中的 state 状态
    int c = getState();
    // 2、如果 state 为 0,证明锁没有被其他线程占用
    if (c == 0) {
        // 2.1、通过 CAS 对 state 进行更新
        if (compareAndSetState(0, acquires)) {
            // 2.2、如果 CAS 更新成功,就将锁的持有者设置为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 3、如果当前线程和锁的持有线程相同,说明发生了「锁的重入」
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 3.1、将锁的重入次数加 1
        setState(nextc);
        return true;
    }
    // 4、如果锁被其他线程占用,就返回 false,表示获取锁失败
    return false;
}
// ReentrantLock
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 1、获取 AQS 中的 state 状态
    int c = getState();
    // 2、如果 state 为 0,证明锁没有被其他线程占用
    if (c == 0) {
        // 2.1、通过 CAS 对 state 进行更新
        if (compareAndSetState(0, acquires)) {
            // 2.2、如果 CAS 更新成功,就将锁的持有者设置为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 3、如果当前线程和锁的持有线程相同,说明发生了「锁的重入」
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 3.1、将锁的重入次数加 1
        setState(nextc);
        return true;
    }
    // 4、如果锁被其他线程占用,就返回 false,表示获取锁失败
    return false;
}

在 nonfairTryAcquire() 方法内部,主要通过两个核心操作去完成资源的获取:

nonfairTryAcquire()

通过 CAS 更新 state 变量。state == 0 表示资源没有被占用。state > 0 表示资源被占用,此时 state 表示重入次数。

CAS
state
state == 0
state > 0
state

通过 setExclusiveOwnerThread() 设置持有资源的线程。

setExclusiveOwnerThread()

如果线程更新 state 变量成功,就表明获取到了资源, 因此将持有资源的线程设置为当前线程即可。

state

addWaiter() 分析

addWaiter() 分析

addWaiter()

在通过 tryAcquire() 方法尝试获取资源失败之后,会调用 addWaiter() 方法将当前线程封装为 Node 节点加入 AQS 内部的队列中。addWaite() 代码如下:

tryAcquire()
addWaiter()
AQS
addWaite()
// AQS
private Node addWaiter(Node mode) {
    // 1、将当前线程封装为 Node 节点。
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 2、如果 pred != null,则证明 tail 节点已经被初始化,直接将 Node 节点加入队列即可。
    if (pred != null) {
        node.prev = pred;
        // 2.1、通过 CAS 控制并发安全。
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 3、初始化队列,并将新创建的 Node 节点加入队列。
    enq(node);
    return node;
}
// AQS
private Node addWaiter(Node mode) {
    // 1、将当前线程封装为 Node 节点。
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 2、如果 pred != null,则证明 tail 节点已经被初始化,直接将 Node 节点加入队列即可。
    if (pred != null) {
        node.prev = pred;
        // 2.1、通过 CAS 控制并发安全。
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 3、初始化队列,并将新创建的 Node 节点加入队列。
    enq(node);
    return node;
}

节点入队的并发安全:

在 addWaiter() 方法中,需要执行 Node 节点 入队 的操作。由于是在多线程环境下,因此需要通过 CAS 操作保证并发安全。

addWaiter()
CAS

通过 CAS 操作去更新 tail 指针指向新入队的 Node 节点,CAS 可以保证只有一个线程会成功修改 tail 指针,以此来保证 Node 节点入队时的并发安全。

CAS
tail
CAS
tail

AQS 内部队列的初始化:

在执行 addWaiter() 时,如果发现 pred == null,即 tail 指针为 null,则证明队列没有初始化,需要调用 enq() 方法初始化队列,并将 Node 节点加入到初始化后的队列中,代码如下:

addWaiter()
pred == null
tail
enq()
Node
// AQS
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            // 1、通过 CAS 操作保证队列初始化的并发安全
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 2、与 addWaiter() 方法中节点入队的操作相同
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
// AQS
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            // 1、通过 CAS 操作保证队列初始化的并发安全
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 2、与 addWaiter() 方法中节点入队的操作相同
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在 enq() 方法中初始化队列,在初始化过程中,也需要通过 CAS 来保证并发安全。

enq()
CAS

初始化队列总共包含两个步骤:初始化 head 节点、tail 指向 head 节点。

head
tail
head

初始化后的队列如下图所示:

acquireQueued() 分析

acquireQueued() 分析

acquireQueued()

为了方便阅读,这里再贴一下 AQS 中 acquire() 获取资源的代码:

AQS
acquire()
// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在 acquire() 方法中,通过 addWaiter() 方法将 Node 节点加入队列之后,就会调用 acquireQueued() 方法。代码如下:

acquire()
addWaiter()
Node
acquireQueued()
// AQS:令队列中的节点尝试获取锁,并且对线程进行阻塞。
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 1、尝试获取锁。
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 2、判断线程是否可以阻塞,如果可以,则阻塞当前线程。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 3、如果获取锁失败,就会取消获取锁,将节点状态更新为 CANCELLED。
        if (failed)
            cancelAcquire(node);
    }
}
// AQS:令队列中的节点尝试获取锁,并且对线程进行阻塞。
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 1、尝试获取锁。
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 2、判断线程是否可以阻塞,如果可以,则阻塞当前线程。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 3、如果获取锁失败,就会取消获取锁,将节点状态更新为 CANCELLED。
        if (failed)
            cancelAcquire(node);
    }
}

在 acquireQueued() 方法中,主要做两件事情:

acquireQueued()

尝试获取资源: 当前线程加入队列之后,如果发现前继节点是 head 节点,说明当前线程是队列中第一个等待的节点,于是调用 tryAcquire() 尝试获取资源。

尝试获取资源: 当前线程加入队列之后,如果发现前继节点是 head 节点,说明当前线程是队列中第一个等待的节点,于是调用 tryAcquire() 尝试获取资源。

head
tryAcquire()

阻塞当前线程:如果尝试获取资源失败,就需要阻塞当前线程,等待被唤醒之后获取资源。

阻塞当前线程:如果尝试获取资源失败,就需要阻塞当前线程,等待被唤醒之后获取资源。

1、尝试获取资源

在 acquireQueued() 方法中,尝试获取资源总共有 2 个步骤:

acquireQueued()

p == head:表明当前节点的前继节点为 head 节点。此时当前节点为 AQS 队列中的第一个等待节点。

p == head
head

tryAcquire(arg) == true:表明当前线程尝试获取资源成功。

tryAcquire(arg) == true

在成功获取资源之后,就需要将当前线程的节点 从等待队列中移除。移除操作为:将当前等待的线程节点设置为 head 节点(head 节点是虚拟节点,并不参与排队获取资源)。

head
head

2、阻塞当前线程

在 AQS 中,当前节点的唤醒需要依赖于上一个节点。如果上一个节点取消获取锁,它的状态就会变为 CANCELLED,CANCELLED 状态的节点没有获取到锁,也就无法执行解锁操作对当前节点进行唤醒。因此在阻塞当前线程之前,需要跳过 CANCELLED 状态的节点。

AQS
CANCELLED
CANCELLED
CANCELLED

通过 shouldParkAfterFailedAcquire() 方法来判断当前线程节点是否可以阻塞,如下:

shouldParkAfterFailedAcquire()
// AQS:判断当前线程节点是否可以阻塞。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 1、前继节点状态正常,直接返回 true 即可。
    if (ws == Node.SIGNAL)
        return true;
    // 2、ws > 0 表示前继节点的状态异常,即为 CANCELLED 状态,需要跳过异常状态的节点。
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 3、如果前继节点的状态不是 SIGNAL,也不是 CANCELLED,就将状态设置为 SIGNAL。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
// AQS:判断当前线程节点是否可以阻塞。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 1、前继节点状态正常,直接返回 true 即可。
    if (ws == Node.SIGNAL)
        return true;
    // 2、ws > 0 表示前继节点的状态异常,即为 CANCELLED 状态,需要跳过异常状态的节点。
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 3、如果前继节点的状态不是 SIGNAL,也不是 CANCELLED,就将状态设置为 SIGNAL。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire() 方法中的判断逻辑:

shouldParkAfterFailedAcquire()

如果发现前继节点的状态是 SIGNAL,则可以阻塞当前线程。

SIGNAL

如果发现前继节点的状态是 CANCELLED,则需要跳过 CANCELLED 状态的节点。

CANCELLED
CANCELLED

如果发现前继节点的状态不是 SIGNAL 和 CANCELLED,表明前继节点的状态处于正常等待资源的状态,因此将前继节点的状态设置为 SIGNAL,表明该前继节点需要对后续节点进行唤醒。

SIGNAL
CANCELLED
SIGNAL

当判断当前线程可以阻塞之后,通过调用 parkAndCheckInterrupt() 方法来阻塞当前线程。内部使用了 LockSupport 来实现阻塞。LockSupoprt 底层是基于 Unsafe 类来阻塞线程,代码如下:

parkAndCheckInterrupt()
LockSupport
LockSupoprt
Unsafe
// AQS
private final boolean parkAndCheckInterrupt() {
    // 1、线程阻塞到这里
    LockSupport.park(this);
    // 2、线程被唤醒之后,返回线程中断状态
    return Thread.interrupted();
}
// AQS
private final boolean parkAndCheckInterrupt() {
    // 1、线程阻塞到这里
    LockSupport.park(this);
    // 2、线程被唤醒之后,返回线程中断状态
    return Thread.interrupted();
}

为什么在线程被唤醒之后,要返回线程的中断状态呢?

在 parkAndCheckInterrupt() 方法中,当执行完 LockSupport.park(this),线程会被阻塞,代码如下:

parkAndCheckInterrupt()
LockSupport.park(this)
// AQS
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    // 线程被唤醒之后,需要返回线程中断状态
    return Thread.interrupted();
}
// AQS
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    // 线程被唤醒之后,需要返回线程中断状态
    return Thread.interrupted();
}

当线程被唤醒之后,需要执行 Thread.interrupted() 来返回线程的中断状态,这是为什么呢?

Thread.interrupted()

这个和线程的中断协作机制有关系,线程被唤醒之后,并不确定是被中断唤醒,还是被 LockSupport.unpark() 唤醒,因此需要通过线程的中断状态来判断。

LockSupport.unpark()

在 acquire() 方法中,为什么需要调用 selfInterrupt()?

acquire()
selfInterrupt()

acquire() 方法代码如下:

acquire()
// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在 acquire() 方法中,当 if 语句的条件返回 true 后,就会调用 selfInterrupt(),该方法会中断当前线程,为什么需要中断当前线程呢?

acquire()
if
true
selfInterrupt()

当 if 判断为 true 时,需要 tryAcquire() 返回 false,并且 acquireQueued() 返回 true。

if
true
tryAcquire()
false
acquireQueued()
true

其中 acquireQueued() 方法返回的是线程被唤醒之后的 中断状态,通过执行 Thread.interrupted() 来返回。该方法在返回中断状态的同时,会清除线程的中断状态。

acquireQueued()
Thread.interrupted()

因此如果 if 判断为 true,表明线程的中断状态为 true,但是调用 Thread.interrupted() 之后,线程的中断状态被清除为 false,因此需要重新执行 selfInterrupt() 来重新设置线程的中断状态。

if
true
true
Thread.interrupted()
false
selfInterrupt()

AQS 资源释放源码分析(独占模式)

AQS 资源释放源码分析(独占模式)

AQS 中以独占模式释放资源的入口方法是 release(),代码如下:

release()
// AQS
public final boolean release(int arg) {
    // 1、尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 2、唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// AQS
public final boolean release(int arg) {
    // 1、尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 2、唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

在 release() 方法中,主要做两件事:尝试释放锁和唤醒后继节点。对应方法如下:

release()

1、尝试释放锁

通过 tryRelease() 方法尝试释放锁,该方法为模板方法,由自定义同步器实现,因此这里仍然以 ReentrantLock 为例来讲解。

tryRelease()
ReentrantLock

ReentrantLock 中实现的 tryRelease() 方法如下:

ReentrantLock
tryRelease()
// ReentrantLock
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 1、判断持有锁的线程是否为当前线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 2、如果 state 为 0,则表明当前线程已经没有重入次数。因此将 free 更新为 true,表明该线程会释放锁。
    if (c == 0) {
        free = true;
        // 3、更新持有资源的线程为 null
        setExclusiveOwnerThread(null);
    }
    // 4、更新 state 值
    setState(c);
    return free;
}
// ReentrantLock
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 1、判断持有锁的线程是否为当前线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 2、如果 state 为 0,则表明当前线程已经没有重入次数。因此将 free 更新为 true,表明该线程会释放锁。
    if (c == 0) {
        free = true;
        // 3、更新持有资源的线程为 null
        setExclusiveOwnerThread(null);
    }
    // 4、更新 state 值
    setState(c);
    return free;
}

在 tryRelease() 方法中,会先计算释放锁之后的 state 值,判断 state 值是否为 0。

tryRelease()
state
state

如果 state == 0,表明该线程没有重入次数了,更新 free = true,并修改持有资源的线程为 null,表明该线程完全释放这把锁。

state == 0
free = true

如果 state != 0,表明该线程还存在重入次数,因此不更新 free 值,free 值为 false 表明该线程没有完全释放这把锁。

state != 0
free
free
false

之后更新 state 值,并返回 free 值,free 值表明线程是否完全释放锁。

state
free
free

2、唤醒后继节点

如果 tryRelease() 返回 true,表明线程已经没有重入次数了,锁已经被完全释放,因此需要唤醒后继节点。

tryRelease()
true

在唤醒后继节点之前,需要判断是否可以唤醒后继节点,判断条件为: h != null && h.waitStatus != 0。这里解释一下为什么要这样判断:

h != null && h.waitStatus != 0

h == null:表明 head 节点还没有被初始化,也就是 AQS 中的队列没有被初始化,因此无法唤醒队列中的线程节点。

h == null
head

h != null && h.waitStatus == 0:表明头节点刚刚初始化完毕(节点的初始化状态为 0),后继节点线程还没有成功入队,因此不需要对后续节点进行唤醒。(当后继节点入队之后,会将前继节点的状态修改为 SIGNAL,表明需要对后继节点进行唤醒)

h != null && h.waitStatus == 0
SIGNAL

h != null && h.waitStatus != 0:其中 waitStatus 有可能大于 0,也有可能小于 0。其中 > 0 表明节点已经取消等待获取资源,< 0 表明节点处于正常等待状态。

h != null && h.waitStatus != 0
waitStatus
> 0
< 0

接下来进入 unparkSuccessor() 方法查看如何唤醒后继节点:

unparkSuccessor()
// AQS:这里的入参 node 为队列的头节点(虚拟头节点)
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 1、将头节点的状态进行清除,为后续的唤醒做准备。
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    // 2、如果后继节点异常,则需要从 tail 向前遍历,找到正常状态的节点进行唤醒。
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 3、唤醒后继节点
        LockSupport.unpark(s.thread);
}
// AQS:这里的入参 node 为队列的头节点(虚拟头节点)
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 1、将头节点的状态进行清除,为后续的唤醒做准备。
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    // 2、如果后继节点异常,则需要从 tail 向前遍历,找到正常状态的节点进行唤醒。
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 3、唤醒后继节点
        LockSupport.unpark(s.thread);
}

在 unparkSuccessor() 中,如果头节点的状态 < 0(在正常情况下,只要有后继节点,头节点的状态应该为 SIGNAL,即 -1),表示需要对后继节点进行唤醒,因此这里提前清除头节点的状态标识,将状态修改为 0,表示已经执行了对后续节点唤醒的操作。

unparkSuccessor()
< 0
SIGNAL

如果 s == null 或者 s.waitStatus > 0,表明后继节点异常,此时不能唤醒异常节点,而是要找到正常状态的节点进行唤醒。

s == null
s.waitStatus > 0

因此需要从 tail 指针向前遍历,来找到第一个状态正常(waitStatus <= 0)的节点进行唤醒。

tail
waitStatus <= 0

为什么要从 tail 指针向前遍历,而不是从 head 指针向后遍历,寻找正常状态的节点呢?

tail
head

遍历的方向和 节点的入队操作 有关。入队方法如下:

// AQS:节点入队方法
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        // 1、先修改 prev 指针。
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            // 2、再修改 next 指针。
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
// AQS:节点入队方法
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        // 1、先修改 prev 指针。
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            // 2、再修改 next 指针。
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

在 addWaiter() 方法中,node 节点入队需要修改 node.prev 和 pred.next 两个指针,但是这两个操作并不是 原子操作,先修改了 node.prev 指针,之后才修改 pred.next 指针。

addWaiter()
node
node.prev
pred.next
node.prev
pred.next

在极端情况下,可能会出现 head 节点的下一个节点状态为 CANCELLED,此时新入队的节点仅更新了 node.prev 指针,还未更新 pred.next 指针,如下图:

head
CANCELLED
node.prev
pred.next

这样如果从 head 指针向后遍历,无法找到新入队的节点,因此需要从 tail 指针向前遍历找到新入队的节点。

head
tail

图解 AQS 工作原理(独占模式)

图解 AQS 工作原理(独占模式)

至此,AQS 中以独占模式获取资源、释放资源的源码就讲完了。为了对 AQS 的工作原理、节点状态变化有一个更加清晰的认识,接下来会通过画图的方式来了解整个 AQS 的工作原理。

由于 AQS 是底层同步工具,获取和释放资源的方法并没有提供具体实现,因此这里基于 ReentrantLock 来画图进行讲解。

ReentrantLock

假设总共有 3 个线程尝试获取锁,线程分别为 T1、 T2 和 T3。

T1
T2
T3

此时,假设线程 T1 先获取到锁,线程 T2 排队等待获取锁。在线程 T2 进入队列之前,需要对 AQS 内部队列进行初始化。head 节点在初始化后状态为 0。AQS 内部初始化后的队列如下图:

T1
T2
T2
head
0

此时,线程 T2 尝试获取锁。由于线程 T1 持有锁,因此线程 T2 会进入队列中等待获取锁。同时会将前继节点(head 节点)的状态由 0 更新为 SIGNAL,表示需要对 head 节点的后继节点进行唤醒。此时,AQS 内部队列如下图所示:

T2
T1
T2
head
0
SIGNAL
head

此时,线程 T3 尝试获取锁。由于线程 T1 持有锁,因此线程 T3 会进入队列中等待获取锁。同时会将前继节点(线程 T2 节点)的状态由 0 更新为 SIGNAL,表示线程 T2 节点需要对后继节点进行唤醒。此时,AQS 内部队列如下图所示:

T3
T1
T3
T2
0
SIGNAL
T2

此时,假设线程 T1 释放锁,会唤醒后继节点 T2。线程 T2 被唤醒后获取到锁,并且会从等待队列中退出。

T1
T2
T2

这里线程 T2 节点退出等待队列并不是直接从队列移除,而是令线程 T2 节点成为新的 head 节点,以此来退出资源获取的等待。此时 AQS 内部队列如下所示:

T2
T2
head

此时,假设线程 T2 释放锁,会唤醒后继节点 T3。线程 T3 获取到锁之后,同样也退出等待队列,即将线程 T3 节点变为 head 节点来退出资源获取的等待。此时 AQS 内部队列如下所示:

T2
T3
T3
T3
head

AQS 资源获取源码分析(共享模式)

AQS 资源获取源码分析(共享模式)

AQS 中以共享模式获取资源的入口方法是 acquireShared(),如下:

acquireShared()
// AQS
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
// AQS
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

在 acquireShared() 方法中,会先尝试获取共享锁,如果获取失败,则将当前线程加入到队列中阻塞,等待唤醒后尝试获取共享锁,分别对应一下两个方法:tryAcquireShared() 和 doAcquireShared()。

acquireShared()
tryAcquireShared()
doAcquireShared()

其中 tryAcquireShared() 方法是 AQS 提供的模板方法,由同步器来实现具体逻辑。因此这里以 Semaphore 为例,来分析共享模式下,如何获取资源。

tryAcquireShared()
Semaphore

tryAcquireShared() 分析

tryAcquireShared() 分析

tryAcquireShared()

Semaphore 中实现了公平锁和非公平锁,接下来以非公平锁为例来分析 tryAcquireShared() 源码。

Semaphore
tryAcquireShared()

Semaphore 中重写的 tryAcquireShared() 方法会调用下边的 nonfairTryAcquireShared() 方法:

Semaphore
tryAcquireShared()
nonfairTryAcquireShared()
// Semaphore 重写 AQS 的模板方法
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

// Semaphore
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        // 1、获取可用资源数量。
        int available = getState();
        // 2、计算剩余资源数量。
        int remaining = available - acquires;
        // 3、如果剩余资源数量 < 0,则说明资源不足,直接返回;如果 CAS 更新 state 成功,则说明当前线程获取到了共享资源,直接返回。
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
// Semaphore 重写 AQS 的模板方法
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

// Semaphore
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        // 1、获取可用资源数量。
        int available = getState();
        // 2、计算剩余资源数量。
        int remaining = available - acquires;
        // 3、如果剩余资源数量 < 0,则说明资源不足,直接返回;如果 CAS 更新 state 成功,则说明当前线程获取到了共享资源,直接返回。
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

在共享模式下,AQS 中的 state 值表示共享资源的数量。

state

在 nonfairTryAcquireShared() 方法中,会在死循环中不断尝试获取资源,如果 「剩余资源数不足」 或者 「当前线程成功获取资源」,就退出死循环。方法返回 剩余的资源数量,根据返回值的不同,分为 3 种情况:

nonfairTryAcquireShared()

剩余资源数量 > 0:表示成功获取资源,并且后续的线程也可以成功获取资源。

剩余资源数量 = 0:表示成功获取资源,但是后续的线程无法成功获取资源。

剩余资源数量 < 0:表示获取资源失败。

doAcquireShared() 分析

doAcquireShared() 分析

doAcquireShared()

为了方便阅读,这里再贴一下获取资源的入口方法 acquireShared():

acquireShared()
// AQS
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
// AQS
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

在 acquireShared() 方法中,会先通过 tryAcquireShared() 尝试获取资源。

acquireShared()
tryAcquireShared()

如果发现方法的返回值 < 0,即剩余的资源数小于 0,则表明当前线程获取资源失败。因此会进入 doAcquireShared() 方法,将当前线程加入到 AQS 队列进行等待。如下:

< 0
doAcquireShared()
// AQS
private void doAcquireShared(int arg) {
    // 1、将当前线程加入到队列中等待。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 2、如果当前线程是等待队列的第一个节点,则尝试获取资源。
                int r = tryAcquireShared(arg);
                if (r >= 0) {
					// 3、将当前线程节点移出等待队列,并唤醒后续线程节点。
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 3、如果获取资源失败,就会取消获取资源,将节点状态更新为 CANCELLED。
        if (failed)
            cancelAcquire(node);
    }
}
// AQS
private void doAcquireShared(int arg) {
    // 1、将当前线程加入到队列中等待。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 2、如果当前线程是等待队列的第一个节点,则尝试获取资源。
                int r = tryAcquireShared(arg);
                if (r >= 0) {
					// 3、将当前线程节点移出等待队列,并唤醒后续线程节点。
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 3、如果获取资源失败,就会取消获取资源,将节点状态更新为 CANCELLED。
        if (failed)
            cancelAcquire(node);
    }
}

由于当前线程已经尝试获取资源失败了,因此在 doAcquireShared() 方法中,需要将当前线程封装为 Node 节点,加入到队列中进行等待。

doAcquireShared()

以 共享模式 获取资源和 独占模式 获取资源最大的不同之处在于:共享模式下,资源的数量可能会大于 1,即可以多个线程同时持有资源。

因此在共享模式下,当线程线程被唤醒之后,获取到了资源,如果发现还存在剩余资源,就会尝试唤醒后边的线程去尝试获取资源。对应的 setHeadAndPropagate() 方法如下:

setHeadAndPropagate()
// AQS
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    // 1、将当前线程节点移出等待队列。
    setHead(node);
	// 2、唤醒后续等待节点。
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
// AQS
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    // 1、将当前线程节点移出等待队列。
    setHead(node);
	// 2、唤醒后续等待节点。
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

在 setHeadAndPropagate() 方法中,唤醒后续节点需要满足一定的条件,主要需要满足 2 个条件:

setHeadAndPropagate()

propagate > 0:propagate 代表获取资源之后剩余的资源数量,如果 > 0,则可以唤醒后续线程去获取资源。

propagate > 0
propagate
> 0

h.waitStatus < 0:这里的 h 节点是执行 setHead() 之前的 head 节点。判断 head.waitStatus 时使用 < 0,主要为了确定 head 节点的状态为 SIGNAL 或 PROPAGATE。如果 head 节点为 SIGNAL,则可以唤醒后续节点;如果 head 节点状态为 PROPAGATE,也可以唤醒后续节点(这是为了解决并发场景下出现的问题,后续会细讲)。

h.waitStatus < 0
h
setHead()
head
head.waitStatus
< 0
head
SIGNAL
PROPAGATE
head
SIGNAL
head
PROPAGATE

代码中关于 唤醒后续等待节点 的 if 判断稍微复杂一些,这里来讲一下为什么这样写:

if
if (propagate > 0 || h == null || h.waitStatus < 0 ||
    (h = head) == null || h.waitStatus < 0)
if (propagate > 0 || h == null || h.waitStatus < 0 ||
    (h = head) == null || h.waitStatus < 0)

h == null || h.waitStatus < 0 : h == null 用于防止空指针异常。正常情况下 h 不会为 null ,因为执行到这里之前,当前节点已经加入到队列中了,队列不可能还没有初始化。h.waitStatus < 0 主要判断 head 节点的状态是否为 SIGNAL 或者 PROPAGATE,直接使用 < 0 来判断比较方便。

h == null || h.waitStatus < 0 : h == null 用于防止空指针异常。正常情况下 h 不会为 null ,因为执行到这里之前,当前节点已经加入到队列中了,队列不可能还没有初始化。

h == null || h.waitStatus < 0
h == null
null

h.waitStatus < 0 主要判断 head 节点的状态是否为 SIGNAL 或者 PROPAGATE,直接使用 < 0 来判断比较方便。

h.waitStatus < 0
head
SIGNAL
PROPAGATE
< 0

(h = head) == null || h.waitStatus < 0 :如果到这里说明之前判断的 h.waitStatus < 0 ,说明存在并发。同时存在其他线程在唤醒后续节点,已经将 head 节点的值由 SIGNAL 修改为 0 了。因此,这里重新获取新的 head 节点,这次获取的 head 节点为通过 setHead() 设置的当前线程节点,之后再次判断 waitStatus 状态。

(h = head) == null || h.waitStatus < 0 :如果到这里说明之前判断的 h.waitStatus < 0 ,说明存在并发。

(h = head) == null || h.waitStatus < 0
h.waitStatus < 0

同时存在其他线程在唤醒后续节点,已经将 head 节点的值由 SIGNAL 修改为 0 了。因此,这里重新获取新的 head 节点,这次获取的 head 节点为通过 setHead() 设置的当前线程节点,之后再次判断 waitStatus 状态。

head
SIGNAL
0
head
head
setHead()
waitStatus

如果 if 条件判断通过,就会走到 doReleaseShared() 方法唤醒后续等待节点,如下:

if
doReleaseShared()
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 1、队列中至少需要一个等待的线程节点。
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 2、如果 head 节点的状态为 SIGNAL,则可以唤醒后继节点。
            if (ws == Node.SIGNAL) {
                // 2.1 清除 head 节点的 SIGNAL 状态,更新为 0。表示已经唤醒该节点的后继节点了。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 2.2 唤醒后继节点
                unparkSuccessor(h);
            }
            // 3、如果 head 节点的状态为 0,则更新为 PROPAGATE。这是为了解决并发场景下存在的问题,接下来会细讲。
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)
            break;
    }
}
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 1、队列中至少需要一个等待的线程节点。
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 2、如果 head 节点的状态为 SIGNAL,则可以唤醒后继节点。
            if (ws == Node.SIGNAL) {
                // 2.1 清除 head 节点的 SIGNAL 状态,更新为 0。表示已经唤醒该节点的后继节点了。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 2.2 唤醒后继节点
                unparkSuccessor(h);
            }
            // 3、如果 head 节点的状态为 0,则更新为 PROPAGATE。这是为了解决并发场景下存在的问题,接下来会细讲。
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)
            break;
    }
}

在 doReleaseShared() 方法中,会判断 head 节点的 waitStatus 状态来决定接下来的操作,有两种情况:

doReleaseShared()
head
waitStatus

head 节点的状态为 SIGNAL:表明 head 节点存在后继节点需要唤醒,因此通过 CAS 操作将 head 节点的 SIGNAL 状态更新为 0。通过清除 SIGNAL 状态来表示已经对 head 节点的后继节点进行唤醒操作了。

head
SIGNAL
head
CAS
head
SIGNAL
0
SIGNAL
head

head 节点的状态为 0:表明存在并发情况,需要将 0 修改为 PROPAGATE 来保证在并发场景下可以正常唤醒线程。

head
0
0
PROPAGATE

为什么需要 PROPAGATE 状态?

为什么需要 PROPAGATE 状态?

PROPAGATE

在 doReleaseShared() 释放资源时,第 3 步不太容易