并发编程——07 深入理解AQS之独占锁ReentrantLock源码分析
1 管程——Java同步的设计思想
1.1 简介
-
管程是一套管理共享资源 + 控制线程并发的设计思想 ,核心解决 2 件事:
-
互斥:同一时间,只能让 1 个线程访问共享资源(比如多个线程抢着操作同一个变量,得排队);
-
同步:线程之间得“商量着来”(比如线程 A 要等线程 B 做完某件事,再继续执行);
-
-
管程发展出 3 种模型(Hasen、Hoare、MESA),但实际开发里 MESA 是 Java 等语言的底层逻辑;
-
入口队列→ 解决互斥
-
多个线程想访问管程里的共享资源时,得先进入入口队列排队;
-
管程会保证:同一时间,只有 1 个线程能出队列,真正去操作共享资源 → 这就实现了互斥,避免多线程乱抢共享资源;
-
-
条件变量 + 等待队列→ 解决同步
- 线程执行时,可能遇到需要等待某个条件(比如“数据还没准备好”“资源没释放”),这时候线程会:
- 调用
wait()
→ 自己进入条件变量对应的等待队列 ,并让出管程的访问权,让其他线程能访问管程; - 等条件满足时,其他线程调用
notify()
/notifyAll()
→ 唤醒等待队列里的线程,让它们重新去入口队列排队,争取执行权 → 这就实现了线程之间的同步协作;
- 调用
- 线程执行时,可能遇到需要等待某个条件(比如“数据还没准备好”“资源没释放”),这时候线程会:
-
共享资源 + 操作方法 → 管程的核心资产
-
共享变量:管程要保护的资源(比如多个线程要操作的同一个计数器、同一个对象);
-
方法(如方法 X、Y):线程实际执行的逻辑,里面会操作共享变量,也会用
wait()
/notify()
控制同步;
-
-
-
MESA 最关键的特点是:线程被
notify()
唤醒后,不会直接抢占执行权,而是重新回到入口队列排队。
1.2 Java 管程的两种实现方式
-
Java 里管程思想通过 两种底层机制落地:
-
基于
Object
的 Monitor 机制(和synchronized
绑定)- 隐式、简单:用
synchronized
修饰方法/代码块时,JVM 自动用 Monitor 控制互斥和同步(靠wait()
/notify()
通信); - 缺点:功能少(只有 1 个等待队列),不够灵活;
- 隐式、简单:用
-
基于 AQS(抽象队列同步器)(
java.util.concurrent.locks
包里的Lock
体系)- 显式、灵活:像
ReentrantLock
就是 AQS 实现,能创建 多个Condition
(条件变量),每个Condition
对应独立等待队列,精准控制线程协作;
- 显式、灵活:像
-
-
例:
@Slf4j public class ConditionDemo2 {// ReentrantLock:显式锁,实现互斥(控制线程排队访问共享资源)private static final ReentrantLock lock = new ReentrantLock();// Condition:基于 AQS 的条件变量,每个 Condition 对应一个等待队列 ,让线程按需等待/唤醒private static final Condition condition = lock.newCondition();public static void main(String[] args) throws InterruptedException {new Thread(() -> {log.debug("t1开始执行....");lock.lock(); // 1. 加锁:抢占“访问权”,保证同一时间只有1个线程执行临界区try {log.debug("t1获取锁....");condition.await(); // 2. 等待:释放锁,进入 condition 的等待队列,阻塞自己} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock(); // 3. 解锁:不管是否异常,都要释放锁,让其他线程能抢到log.debug("t1执行完成....");}}, "t1").start();new Thread(() -> {log.debug("t2开始执行....");lock.lock();try {log.debug("t2获取锁....");// 让线程在obj上一直等待下去condition.await();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();log.debug("t2执行完成....");}}, "t2").start();// 主线程先睡 2 秒,等 t1、t2 进入等待队列Thread.sleep(2000);log.debug("准备获取锁,去唤醒 condition上阻塞的线程");lock.lock(); // 先抢锁,才能操作 conditiontry {condition.signalAll(); // 唤醒 condition 等待队列里的所有线程(t1、t2)log.debug("唤醒condition上阻塞的线程");} catch (Exception e) {e.printStackTrace();} finally {lock.unlock(); // 释放锁}} }
-
核心对比:
synchronized
vsReentrantLock + Condition
对比项 synchronized
(Monitor 机制)ReentrantLock + Condition
(AQS 机制)等待队列数量 只有 1 个公共等待队列 可创建 多个 Condition
,每个对应独立队列唤醒精度 只能 notify()
(随机唤醒)/notifyAll()
可精准唤醒某个 Condition
队列的线程(signal()
/signalAll()
)使用复杂度 简单(隐式加锁/释放) 复杂(需手动 lock()
/unlock()
,但更灵活)
2 AQS原理分析
2.1 简介
-
AQS 是 Java 并发包(
java.util.concurrent
)里的同步器框架 ,把“锁、同步工具(比如 CountDownLatch、Semaphore)”的通用逻辑(比如“排队等待”“抢锁”)抽象出来,让开发者不用重复写底层同步代码;- 打个比方:当想编写代码实现“锁”“计数器”这些同步工具的时候,不用自己实现“线程怎么排队”“怎么抢资源”等等,直接基于 AQS 改一改就行 → AQS 是这些同步工具的底层模板;
-
AQS 的核心作用:统一“同步行为”。Java 并发包里的同步工具(
Lock
、CountDownLatch
、Semaphore
等),看起来功能不同,但底层都要解决线程怎么协调访问共享资源 的问题。AQS 把这些通用逻辑(比如下面这些)抽象成框架:-
等待队列:线程抢不到资源,得排队等待;
-
条件队列:线程需要“等某个条件满足”(比如 CountDownLatch 要等计数器归 0),得进条件队列等通知;
-
独占/共享获取:有些资源只能“一个线程用”(比如 ReentrantLock 的独占锁),有些可以“多个线程一起用”(比如 Semaphore 的共享锁);
-
-
想基于 AQS 做一个同步器(比如自己写个锁),步骤如下:
-
写一个内部类 Sync,继承 AQS:比如
ReentrantLock
里的FairSync
(公平锁)、NonfairSync
(非公平锁),都是继承 AQS 的内部类; -
把同步器的方法,映射到 Sync 的方法:比如你写了个自定义锁,调用
lock()
时,实际是让 Sync 里的acquire()
方法去抢资源;调用unlock()
时,让release()
方法释放资源 → AQS 已经帮你把“抢资源、排队、唤醒”这些逻辑写好了,你只需填业务细节;
-
-
AQS 内置了这些“通用同步逻辑”,你基于它做的同步器,自动拥有这些特性:
-
阻塞等待队列:线程抢不到资源,会被放进一个 CLH 队列(一种高效的等待队列)里阻塞,避免空转浪费 CPU;
-
共享/独占模式:
- 独占:比如
ReentrantLock
的独占锁,同一时间只能一个线程用; - 共享:比如
Semaphore
允许多个线程同时获取许可;
- 独占:比如
-
公平/非公平策·略:
- 公平:严格按“线程排队顺序”抢资源(比如
ReentrantLock
的公平锁); - 非公平:新来的线程可以直接“插队”抢资源(默认更高效,比如
ReentrantLock
的非公平锁);
- 公平:严格按“线程排队顺序”抢资源(比如
-
可重入:线程拿到锁后,能多次进入(比如
ReentrantLock
支持重入,避免自己锁自己); -
允许中断:线程等待时,支持被中断(比如
lockInterruptibly()
方法);
-
-
你常用的并发工具,几乎都基于 AQS:
-
ReentrantLock
(锁)→ 用 AQS 实现“独占、重入、公平/非公平”; -
CountDownLatch
(计数器)→ 用 AQS 实现“线程等待计数器归 0”; -
Semaphore
(信号量)→ 用 AQS 实现“多线程共享资源访问”。
-
2.2 核心结构
-
核心变量:
private volatile int state;
-
AQS 用
state
表示共享资源的可用状态 (比如锁是否被占用、剩余许可数量等);- 例子:
ReentrantLock
里,state=0
表示锁空闲;state>0
表示锁被占用(值也代表“重入次数”); - 例子:
Semaphore
里,state
直接表示“剩余可用许可数”;
- 例子:
-
volatile
关键字:保证state
的修改对所有线程“可见”(避免多线程下的“脏读”问题);
-
-
AQS 给
state
提供了 3 种访问方式:protected final int getState() {return state; }
- 简单读
state
的值,因为state
是volatile
,读操作本身是线程安全的;
protected final void setState(int newState) {state = newState; }
- 直接写
state
,看似普通,但要注意:只有当前线程已独占资源时,才能安全调用 (比如ReentrantLock
拿到锁的线程,才能直接改state
);
protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
- CAS 操作(底层靠
Unsafe
类实现),核心逻辑:如果当前state
的值等于expect
(期望值),就把它改成update
,返回成功;否则不修改,返回失败;
- 简单读
-
AQS 支持 两种资源抢占方式,应对不同同步需求:
- 独占模式(Exclusive)
- 特点:同一时间,只有 1 个线程能拿到资源(比如
ReentrantLock
的独占锁); - 对应方法:
tryAcquire(int)
(抢资源)、tryRelease(int)
(释放资源);
- 特点:同一时间,只有 1 个线程能拿到资源(比如
- 共享模式(Share)
- 特点:同一时间,多个线程能拿到资源(比如
Semaphore
允许多个线程拿许可,CountDownLatch
让多个线程等待同一个计数器); - 对应方法:
tryAcquireShared(int)
(抢资源,返回值有特殊含义)、tryReleaseShared(int)
(释放资源);
- 特点:同一时间,多个线程能拿到资源(比如
- 独占模式(Exclusive)
-
AQS 是 抽象类,想基于它实现同步器,必须重写下面这些方法(“钩子方法”):
方法名 作用 isHeldExclusively()
判断当前线程是否“独占”资源(一般 Condition
相关场景用)tryAcquire(int)
【独占模式】尝试抢资源:成功(返回 true)或失败(返回 false) tryRelease(int)
【独占模式】尝试释放资源:成功(返回 true)或失败(返回 false) tryAcquireShared(int)
【共享模式】尝试抢资源:负数 → 失败;0 → 成功,但没剩余资源;正数 → 成功,且有剩余资源 tryReleaseShared(int)
【共享模式】尝试释放资源:成功允许唤醒后续等待线程(返回 true),否则返回 false
2.3 AQS 的两大核心队列
-
AQS 里有两种队列:
-
同步等待队列(CLH 队列):处理“线程抢资源失败,排队等锁”的场景;
-
条件等待队列:处理“线程等某个条件满足(比如
Condition.await()
)”的场景;
-
-
AQS 的
Node
节点有 5 种状态(用int
表示),用来标记线程“在队列里是啥情况”:状态值 含义 0(初始化状态) 节点在同步队列里,等着抢资源(锁) CANCELLED(1) 线程被取消(比如超时、被中断),节点要被清理出队列 SIGNAL(-1) 节点的后继节点需要被唤醒(当前节点释放资源后,要唤醒下一个节点) CONDITION(-2) 节点在条件队列里,等着“条件满足” PROPAGATE(-3) 共享模式下,后续节点需要被唤醒(比如 Semaphore
释放许可时,批量唤醒)
2.3.1 同步等待队列
-
AQS中的同步等待队列也称 CLH 队列,CLH 队列是 Craig、Landin、Hagersten 三人发明的一种基于双向链表实现的 FIFO(先进先出)队列,Java中的 CLH 队列是原 CLH 队列的一个变种,线程由原自旋机制改为阻塞机制;
-
作用:线程抢资源(比如锁)失败时,被包装成
Node
节点,扔进这个队列里“排队等唤醒”; -
AQS 依赖 CLH 同步队列来完成同步状态的管理:
- 入队:线程抢资源失败 → 被包装成
Node
→ 加入 CLH 队列尾部 → 线程阻塞(不占 CPU); - 出队:持有资源的线程释放资源 → 唤醒队列头部的线程(公平锁) → 头部线程重新抢资源(成功就出队,继续执行);
- 入队:线程抢资源失败 → 被包装成
-
举个栗子(比如
ReentrantLock
抢锁):-
线程 1 拿到锁 →
state
标记为“占用”; -
线程 2、3 来抢锁 → 失败 → 被包装成
Node
,加入 CLH 队列 → 阻塞; -
线程 1 释放锁 → 唤醒队列头部的线程 2 → 线程 2 重新抢锁(成功后
state
标记为“占用”,继续执行)。
-
2.3.2 条件等待队列
-
基于单向链表实现,每个
Condition
(条件变量)对应一个独立的条件队列; -
作用:线程执行到
condition.await()
时,会从同步队列转移到条件队列,等待某个条件触发(比如condition.signal()
); -
核心流程:
-
入队:线程在同步队列中拿到锁 → 执行
condition.await()
→ 释放锁 → 从同步队列移除 → 加入条件队列 → 线程阻塞; -
出队:其他线程执行
condition.signal()
→ 条件队列里的线程被转移回同步队列 → 重新排队抢锁 → 抢到锁后继续执行;
-
-
举个栗子(比如
ReentrantLock + Condition
):-
线程 A 拿到锁 → 执行
condition.await()
→ 释放锁 → 从同步队列移到条件队列 → 阻塞; -
线程 B 拿到锁 → 执行
condition.signal()
→ 线程 A 从条件队列移回同步队列 → 线程 A 重新排队抢锁 → 抢到后继续执行;
-
2.3.3 两个队列的关系
- 线程会在 同步队列 ↔ 条件队列 之间转移:
condition.await()
→ 同步队列 → 条件队列(线程等条件);condition.signal()
→ 条件队列 → 同步队列(条件满足,线程重新抢资源);
- 核心目的:让线程“按需等待” ,避免无效竞争(比如条件不满足时,没必要占着同步队列的位置)。
2.4 基于AQS实现一把独占锁
/*** 基于 AQS 实现一把独占锁* * 核心思路:* 1. 继承 AbstractQueuedSynchronizer(AQS),复用其队列管理、阻塞唤醒等能力* 2. 重写 tryAcquire/tryRelease 方法,定义「加锁」和「释放锁」的具体规则* 3. 暴露 lock/unlock 等对外 API,让用户可以像使用普通锁一样操作*/
public class TulingLock extends AbstractQueuedSynchronizer {// 1. 重写 AQS 核心方法:定义加锁规则@Overrideprotected boolean tryAcquire(int unused) {// compareAndSetState(0, 1):CAS 操作,尝试把 state 从 0(未占用)改为 1(占用)// state 是 AQS 核心变量,标记锁的状态(0=空闲,1=占用)if (compareAndSetState(0, 1)) {// 如果 CAS 成功,标记当前线程为锁的持有者// setExclusiveOwnerThread 是 AQS 提供的方法,记录「独占模式」下的持有线程setExclusiveOwnerThread(Thread.currentThread());return true; // 加锁成功}return false; // CAS 失败,加锁失败(锁已被占用)}// 2. 重写 AQS 核心方法:定义释放锁规则@Overrideprotected boolean tryRelease(int unused) {// 标记锁的持有者为 null(释放独占线程)setExclusiveOwnerThread(null);// 把 state 置为 0,标记锁回到空闲状态setState(0);return true; // 释放锁成功(AQS 会唤醒等待队列中的线程)}// 3. 暴露对外 API:让用户使用锁/*** 加锁方法:调用 AQS 的 acquire(1)* acquire 是 AQS 提供的模板方法,内部会调用我们重写的 tryAcquire* 如果 tryAcquire 失败,线程会进入同步队列阻塞等待*/public void lock() {acquire(1);}/*** 尝试加锁(非阻塞):直接调用我们重写的 tryAcquire* 成功返回 true,失败返回 false(不会阻塞线程)*/public boolean tryLock() {return tryAcquire(1);}/*** 释放锁方法:调用 AQS 的 release(1)* release 是 AQS 提供的模板方法,内部会调用我们重写的 tryRelease* 释放后会唤醒同步队列中的等待线程*/public void unlock() {release(1);}/*** 辅助方法:判断锁是否被占用* 通过 AQS 的 state 变量判断(state != 0 表示被占用)*/public boolean isLocked() {return getState() != 0;}
}
-
核心逻辑理解读:
-
继承 AQS:
TulingLock extends AbstractQueuedSynchronizer
,直接复用 AQS 的「同步队列管理」「线程阻塞/唤醒」能力,不用自己实现队列和阻塞逻辑; -
tryAcquire
:定义加锁规则- 用
compareAndSetState(0, 1)
做 CAS 操作,保证原子性抢占锁; - 成功后用
setExclusiveOwnerThread
标记当前线程为锁的持有者,实现独占锁(同一时间只有一个线程能持有);
- 用
-
tryRelease
:定义释放规则- 把持有线程置为
null
,state
置为0
,彻底释放锁; - AQS 会自动唤醒同步队列中的等待线程,让它们重新竞争锁;
- 把持有线程置为
-
对外 API(
lock/unlock
等)- 借助 AQS 的模板方法(
acquire/release
),把tryAcquire/tryRelease
封装成用户可用的锁操作; - 让
TulingLock
像ReentrantLock
一样,支持lock()
阻塞加锁、tryLock()
非阻塞加锁等;
- 借助 AQS 的模板方法(
-
-
这段代码本质上就是简易版的
ReentrantLock
(不支持重入):-
ReentrantLock
也基于 AQS 实现,核心逻辑一样; -
差异点:
ReentrantLock
支持可重入(加锁次数和state
关联,当前线程再次加锁不会失败),而这段代码是“非重入”的独占锁(同一线程多次lock()
会阻塞); -
如果想支持可重入,只需修改
tryAcquire
:@Override protected boolean tryAcquire(int unused) {Thread current = Thread.currentThread();int c = getState();if (c == 0) { // 锁空闲if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) { // 同一线程再次加锁(可重入)setState(c + 1); // state 计数 +1return true;}return false; }
-
3 ReentrantLock 源码分析
3.1 简介
-
ReentrantLock 是 Java 里的“显式锁”,基于 AQS 实现,用来解决多线程并发安全问题,功能和
synchronized
类似(保证同一时间只有一个线程执行临界区代码),但更灵活;-
基于 AQS:底层靠 AQS 的“同步队列 + 状态管理”实现线程排队、锁竞争;
-
互斥锁:同一时间,只能有一个线程拿到锁(保证线程安全);
-
可重入:同一个线程可以多次获取同一把锁(不会自己锁死自己);
-
ReentrantLock
比synchronized
更灵活 ,比如:-
支持公平锁/非公平锁 切换(
synchronized
只能非公平); -
支持限时等待锁(
tryLock(time)
避免死等); -
支持多个条件变量(
Condition
),精准控制线程唤醒;
-
-
-
使用方式:
public class ReentrantLockTest {// 1. 创建 ReentrantLock 实例(默认非公平锁,高效)private final ReentrantLock lock = new ReentrantLock(); public void doSomething() {// 2. 加锁:调用 lock(),线程会阻塞直到拿到锁lock.lock(); try {// 3. 临界区:这里的代码,同一时间只有一个线程能执行// 比如操作共享变量、调用非线程安全的方法等// ... method body ...} finally {// 4. 释放锁:必须在 finally 里解锁!// 保证即使临界区抛异常,锁也能释放,避免其他线程死等lock.unlock(); }} }
3.2 原理
- ReentrantLock 基于 AQS 和 CAS 实现。
3.2.1 lock()
-
ReentrantLock 构造时,默认是非公平锁,也可以传
true
显式用公平锁。lock()
执行时会先判断:-
如果是公平锁 → 走
FairSync.lock()
分支- 公平锁的核心特点:必须排队,不能插队,流程更严格,适合需要“公平性”的场景;
-
如果是非公平锁 → 走
NonfairSync.lock()
分支- 非公平锁的核心特点:不排队,直接尝试“插队”抢锁 ,流程更短,适合大部分场景;
-
-
非公平锁流程(默认,更高效。看黄色、紫色流程)
-
上来就用 CAS 把 AQS 的
state
从 0 改成 1 → 成功的话,直接拿到锁,标记当前线程为持有者 → 这一步是“插队”的关键:不管队列里有没有等待线程,新线程先试一次抢锁; -
如果
state != 0
(锁被占用),再判断持有锁的线程是不是当前线程(可重入逻辑):-
是 →
state +1
(重入次数+1); -
不是 → 进入同步队列排队,即把当前线程包装成
Node
,加入 AQS 的同步队列尾部 → 线程阻塞,等待被唤醒;
-
-
-
公平锁流程(严格排队,略低效。看红色、紫色流程)
-
即使
state == 0
(锁空闲),也要先看同步队列里有没有线程在排队 → 有排队的,当前线程必须加入队列,不能直接抢; -
如果队列里没人排队,才用
CAS
抢锁 → 成功则标记线程,失败则进入队列排队; -
和非公平锁类似,包装成
Node
加入队列尾部 → 线程阻塞,等待被唤醒;
-
-
核心差异:插队 vs 排队
对比项 非公平锁(默认) 公平锁 抢锁逻辑 直接 CAS 抢锁(允许插队) 先检查队列,没人排队才 CAS 抢锁 优点 减少线程切换,性能更高 保证线程公平性,避免“线程饿死” 缺点 可能导致线程饿死(一直插队,队列里的线程永远抢不到) 频繁切换线程,性能略低
3.2.2 unlock()
-
流程解读:
-
校验:持有锁的线程是否是当前线程?
- 判断逻辑:ReentrantLock 是独占锁,只有持有锁的线程才能释放锁;
- 如果不是:直接抛
IllegalMonitorStateException
(比如线程 A 没拿到锁,却调用unlock()
); - 如果是:继续下一步;
-
处理重入计数:
state -1
。ReentrantLock 支持可重入,用 AQS 的state
变量记录 重入次数:-
state = 0
:锁空闲;state > 0
:锁被占用(值=重入次数); -
释放时,先把 `state -1;
-
-
判断
state
是否归 0?- 如果
state == 0
:表示当前线程彻底释放了锁(重入次数清零)→ 执行唤醒等待队列中的线程逻辑,让队列里的线程重新竞争锁;
- 如果
-
-
如果
state > 0
:表示当前线程还在重入中(比如连续lock()
了 3 次,只unlock()
了 2 次)→ 只减少重入次数,不唤醒其他线程(因为锁还没完全释放);-
唤醒等待线程。当锁彻底释放(
state == 0
),ReentrantLock 会:-
从 AQS 的同步队列(CLH 队列)中取出头部节点(等待最久的线程);
-
唤醒该线程,让它重新尝试抢锁(调用
acquire()
逻辑);
-
-
-
关键细节
- 可重入的体现:每次
unlock()
只会让state -1
,直到state == 0
才真正释放锁 → 支持“多次加锁,对应多次释放”; - 唤醒的时机:只有当锁彻底释放(
state == 0
)时,才会唤醒等待队列的线程 → 保证锁的“独占性”,避免多个线程同时持有锁; - 异常保护:非持有锁的线程调用
unlock()
会抛异常 → 避免“非法释放锁”导致的线程安全问题。
- 可重入的体现:每次
-
ReentrantLock.unlock()
和synchronized
的“自动释放”逻辑类似,但更“显式”:-
synchronized
靠 JVM 自动释放(退出同步块时),无需手动调用; -
ReentrantLock
必须手动unlock()
,但流程更透明(能看到state
处理和唤醒逻辑)。
-
3.3 源码分析
3.3.1 构造函数
-
ReentrantLock 的构造函数,直接决定了用公平锁还是非公平锁 :
public class ReentrantLock {// 核心:用 Sync 的子类(FairSync/NonfairSync)实现不同策略private final Sync sync; // 1. 默认构造(非公平锁)public ReentrantLock() {sync = new NonfairSync(); // 默认用“非公平锁”,性能更高}// 2. 显式指定公平性public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); } }
-
Sync
是 ReentrantLock 的内部抽象类,继承自 AQS → 复用 AQS 的队列和状态管理; -
FairSync
(公平锁)和NonfairSync
(非公平锁)是Sync
的子类 → 分别实现“公平”和“非公平”的加锁逻辑。
-
3.3.2 lock()
-
ReentrantLock 的
lock()
很简单,直接调用sync.lock()
:public void lock() {sync.lock(); // 具体逻辑由 FairSync/NonfairSync 决定 }
-
公平锁 vs 非公平锁 的差异,全在
Sync
子类的lock()
实现里; -
公平锁(FairSync)的
lock()
逻辑final void lock() {acquire(1); // 调用 AQS 的 acquire 方法 }
-
公平锁的核心:严格排队,不允许插队 ,必须尊重“等待队列”的顺序;
-
acquire(1)
是 AQS 的模板方法,内部会调用tryAcquire
(需要子类实现); -
FairSync 的
tryAcquire
会 先检查“同步队列是否有等待线程” ,再尝试 CAS 抢锁 → 保证公平性;
-
-
非公平锁(NonfairSync)的
lock()
逻辑final void lock() {// 1. 直接 CAS 抢锁(插队):把 state 从 0 改成 1if (compareAndSetState(0, 1)) {// 抢锁成功,标记当前线程为持有者setExclusiveOwnerThread(Thread.currentThread());} else {// 2. 抢锁失败,调用 AQS 的 acquire 排队acquire(1);} }
-
非公平锁的核心:允许插队,直接抢锁 ,性能更高(减少线程切换);
-
上来就用
CAS
抢锁(不管队列里有没有线程)→ 这是“非公平”的核心(插队); -
如果 CAS 失败(锁被占用或有竞争),才会进入 AQS 的队列排队 → 退而求其次。
-
3.3.3 acquire()
-
acquire(int arg)
是 AQS 提供的模板方法,定义了抢占资源(加锁)的通用流程:-
先尝试抢锁(调用
tryAcquire
,由子类实现,比如 ReentrantLock 的 FairSync/NonfairSync); -
抢锁失败 → 进入“同步队列”排队 → 阻塞线程,等待被唤醒;
-
-
源码:
public final void acquire(int arg) {// 1. 尝试抢锁:调用子类重写的 tryAcquireif (!tryAcquire(arg) && // 2. 抢锁失败 → 入队 + 阻塞acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 3. 处理中断(可选,标记当前线程被中断)selfInterrupt(); }
-
tryAcquire(arg)
:尝试抢锁(子类自定义逻辑)-
作用:调用子类(如 ReentrantLock 的 FairSync/NonfairSync)重写的
tryAcquire
,尝试抢占资源(锁); -
返回
true
:抢锁成功,直接返回(不执行后续逻辑); -
返回
false
:抢锁失败,进入“入队 + 阻塞”流程;
-
-
addWaiter(Node.EXCLUSIVE)
:把线程包装成 Node,加入同步队列-
Node.EXCLUSIVE
:标记节点为“独占模式”(适合 ReentrantLock 这类独占锁); -
addWaiter
逻辑:- 把当前线程包装成
Node
(AQS 里的双向链表节点); - 尝试快速把
Node
加入同步队列的尾部(优化:先尝试 CAS 加尾节点,失败则遍历队列找尾节点);
- 把当前线程包装成
-
-
acquireQueued(Node node, int arg)
:线程入队后,阻塞等待-
作用:让入队的线程阻塞等待,直到被唤醒并成功抢到锁;
-
核心逻辑:
- 循环检查前驱节点:如果当前节点是队列头部的下一个节点,再次尝试抢锁(
tryAcquire
); - 抢锁成功 → 出队,返回
false
(不执行selfInterrupt
); - 抢锁失败 → 判断是否需要阻塞线程(通过前驱节点的状态,比如
SIGNAL
); - 线程阻塞后,会在“被唤醒、超时、中断”时醒来,再次尝试抢锁;
- 循环检查前驱节点:如果当前节点是队列头部的下一个节点,再次尝试抢锁(
-
-
selfInterrupt()
:标记线程中断-
作用:如果线程在
acquireQueued
中被中断唤醒,调用selfInterrupt()
标记“当前线程被中断”(记录中断状态,不立即处理); -
注意:AQS 的
acquire
不会“响应中断”(不会抛出InterruptedException
),而是让子类自己决定如何处理中断;
-
-
-
核心流程总结
-
“尝试 + 排队 + 阻塞”的意义
-
tryAcquire
快速尝试:避免每次都入队,减少线程切换开销(非公平锁的CAS
抢锁就是典型); -
同步队列(CLH 队列):用双向链表管理等待线程,保证“FIFO”(先进先出),配合
acquireQueued
的循环抢锁,实现高效的“唤醒-竞争”逻辑; -
阻塞与唤醒:通过
LockSupport.park()
阻塞线程,unpark()
唤醒线程,避免无效 CPU 空转;
-
-
和 ReentrantLock 的关联:ReentrantLock 的
lock()
最终调用acquire(1)
,而tryAcquire
的逻辑由FairSync
或NonfairSync
实现-
公平锁:
tryAcquire
会检查队列,保证“排队优先”; -
非公平锁:
tryAcquire
会直接CAS
抢锁(插队),失败再入队。
-
3.3.4 tryAcquire()
-
公平锁(FairSync)的
tryAcquire
:严格排队,不插队-
公平锁的核心是必须尊重等待队列的顺序,即使锁空闲,也要先看有没有线程在队列里排队;
-
源码:
// 公平锁 FairSync#tryAcquire() 方法 protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // AQS的state:0=空闲,>0=占用(重入次数)if (c == 0) { // 锁空闲时// 关键:先检查队列里有没有等待的线程if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current);return true;}} // 重入逻辑:当前线程已持有锁,state+1else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false; }
-
hasQueuedPredecessors()
:判断同步队列中是否有线程在排队;- 如果有 → 即使锁空闲,当前线程也不能直接抢(必须排队);
- 如果没有 → 才能用
CAS
抢锁;
-
重入逻辑和非公平锁一致:当前线程已持有锁时,
state
递增(支持可重入)。
-
-
-
非公平锁(NonfairSync)的
tryAcquire
:允许插队,直接抢-
非公平锁的核心是 “不排队,直接尝试抢锁”,性能更高(减少线程切换);
-
源码:
// 非公平锁 NonFairSync#tryAcquire() 方法 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }// 非公平锁 Sync#nonfairTryAcquire() 方法 final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) { // 锁空闲时// 关键:不检查队列,直接CAS抢锁if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current);return true;}} // 重入逻辑:和公平锁一致else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false; }
-
锁空闲时,不调用
hasQueuedPredecessors()
→ 不管队列里有没有线程,直接用CAS
抢锁; -
这就是非公平的体现:新线程可以“插队”,不用排队,直接抢锁;
-
-
-
ReentrantLock 默认是非公平锁,因为:
-
性能更高:避免了“检查队列”的开销,且“插队抢锁”能减少线程切换(不用频繁入队、阻塞、唤醒);
-
大多数场景不需要“绝对公平”:业务上通常更关注性能,而非严格的排队顺序。
-
3.3.5 addWaiter()
-
addWaiter(Node mode)
的作用是:-
将当前线程包装成 AQS 的
Node
节点(标记为“共享模式”或“独占模式”);SHARED
:共享模式(比如Semaphore
允许多线程同时获取许可);EXCLUSIVE
:独占模式(比如ReentrantLock
同一时间只允许一个线程持有锁);
-
把这个
Node
加入 AQS 的同步队列尾部,等待被唤醒抢锁;
-
-
源码:
private Node addWaiter(Node mode) {// 1. 把当前线程和模式(SHARED/EXCLUSIVE)封装到 Node 中 → 后续队列用 Node 管理线程Node node = new Node(Thread.currentThread(), mode);// 2. 尝试快速入队(先取队列尾节点)Node pred = tail;if (pred != null) { // 队列不为空node.prev = pred; // 当前节点的前驱指向原尾节点// CAS 把尾节点设为当前节点if (compareAndSetTail(pred, node)) {pred.next = node; // 原尾节点的后继指向当前节点return node;}}// 3. 快速入队失败 → 调用 enq 方法,自旋入队enq(node);return node; }
3.3.6 enq()
-
enq
的作用是:不管队列是否为空,都要把节点node
加入同步队列的尾部。如果队列为空,还会先初始化队列; -
源码:
private Node enq(final Node node) {// 1. 自旋:直到节点成功入队for (;;) {Node t = tail; // 获取队列的尾节点// 2. 队列为空 → 初始化队列if (t == null) { // CAS 初始化头节点(空节点)if (compareAndSetHead(new Node())) tail = head; // 头、尾节点都指向这个空节点} else { // 3. 队列不为空 → 把节点加入尾部node.prev = t; // 当前节点的前驱指向原尾节点// CAS 把尾节点设为当前节点if (compareAndSetTail(t, node)) {t.next = node; // 原尾节点的后继指向当前节点 → 完成双向链表的连接return t; // 返回原尾节点}}} }
-
enq
是 AQS 中节点入队的兜底逻辑:-
解决队列为空时的初始化问题;
-
用 自旋 + CAS 保证多线程并发入队时,节点能安全地加入队列尾部,不会出现队列断裂或节点丢失;
-
-
addWaiter
方法会先尝试快速入队(直接 CAS 设尾节点),如果失败,就会调用enq
→enq
用自旋保证一定入队成功。
3.3.7 acquireQueued()
-
acquireQueued
是 AQS 中线程入队后,如何等待锁的核心逻辑,流程是:自旋抢锁 → 抢不到则阻塞 → 被唤醒后再抢; -
源码:
final boolean acquireQueued(final Node node, int arg) {boolean failed = true; // 标记“是否成功获取锁”try {boolean interrupted = false;for (;;) { // 自旋:直到抢到锁或被中断final Node p = node.predecessor(); // 获取前驱节点// 若前驱是头节点(说明自己是的第一个有值的节点) → 尝试抢锁(头节点代表“当前持有锁的线程”)if (p == head && tryAcquire(arg)) {setHead(node); // 抢锁成功 → 把当前节点设为新头节点p.next = null; // 原头节点的next置空,方便GC回收failed = false; // 标记“成功获取锁”return interrupted; // 返回是否被中断过}// 抢锁失败 → 判断是否需要阻塞当前线程if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { // 阻塞线程,返回是否被中断interrupted = true;}}} finally {if (failed) // 若获取锁失败(如异常),取消当前节点的抢锁cancelAcquire(node);} }
-
自旋抢锁:只要前驱是头节点(说明自己是的第一个有值的节点),就尝试
tryAcquire
抢锁(由子类实现,比如 ReentrantLock 的公平/非公平逻辑); -
抢锁成功:更新头节点,断开原头节点引用,保证 GC 回收;
-
抢锁失败:调用
shouldParkAfterFailedAcquire
判断是否要阻塞,若需要则用park
挂起线程,等待被唤醒;
-
-
shouldParkAfterFailedAcquire(Node pred, Node node)
是acquireQueued
的“辅助判断”,核心是检查“前驱节点的状态”,决定当前线程是否该阻塞;-
AQS 的
Node
类用waitStatus
标记节点状态:-
CANCELLED (1)
:节点已取消(比如线程中断、超时),需从队列中移除; -
SIGNAL (-1)
:节点的后继节点需要被唤醒(当前节点释放锁时,要唤醒后继); -
CONDITION (-2)
:节点在“条件队列”中(和Condition
相关,非同步队列逻辑); -
PROPAGATE (-3)
:共享模式下,需要“传播”唤醒信号(比如Semaphore
释放许可时);
-
-
源码:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus; // 获取前驱节点的状态if (ws == Node.SIGNAL) { // 前驱状态是 SIGNAL → 说明当前节点需要被唤醒,所以可以阻塞return true;}if (ws > 0) { // 前驱状态是 CANCELLED → 跳过取消的节点,重新找有效的前驱do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node; // 把当前节点挂到有效前驱下} else { // 前驱状态是 0/CONDITION/PROPAGATE → CAS 把前驱状态设为 SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false; // 暂时不阻塞,下次自旋再判断 }
-
前驱是 SIGNAL:直接返回
true
,当前线程可以阻塞(因为前驱会唤醒它); -
前驱是 CANCELLED:清理“取消的节点”,把当前节点挂到有效前驱下,保证队列有效性;
-
其他状态:通过 CAS 把前驱状态设为 SIGNAL,下次自旋时再判断是否阻塞;
-
-
-
整体流程:线程如何在队列中“等待锁”?
- 线程入队后,进入
acquireQueued
自旋抢锁; - 若前驱是头节点,尝试
tryAcquire
抢锁 → 成功则更新头节点; - 抢锁失败,调用
shouldParkAfterFailedAcquire
检查前驱状态 → 决定是否阻塞; - 若需要阻塞,用
park
挂起线程,直到被唤醒(比如锁释放时,前驱节点调用unpark
)。
- 线程入队后,进入
3.3.8 unlock()
-
ReentrantLock 的
unlock()
代码非常简洁,直接调用sync.release(1)
:public void unlock() {sync.release(1); // 实际逻辑由 AQS 的 release 实现 }
sync
是 ReentrantLock 内部的Sync
子类(FairSync/NonfairSync),但release
方法最终由 AQS 实现;
-
AQS 的
release
是“释放锁”的核心模板方法,流程是:尝试释放锁 → 释放成功则唤醒队列中的等待线程;public final boolean release(int arg) {// 1. 尝试释放锁:调用子类(如 ReentrantLock)重写的 tryReleaseif (tryRelease(arg)) { Node h = head; // 获取同步队列的头节点// 2. 头节点存在,且状态不是 0 → 唤醒后续等待的节点if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒头节点的后继节点return true; // 释放成功}return false; // 释放失败(比如重入次数没清完) }
-
tryRelease(arg)
:- 由子类(如 ReentrantLock)重写,实现具体的释放锁逻辑;
- 对 ReentrantLock 来说,
tryRelease
会减少重入计数(state -1),如果state == 0
,则彻底释放锁(标记当前线程为null
);
-
unparkSuccessor(h)
:当锁彻底释放后,唤醒同步队列中头节点的后继节点;
-
-
整体流程:
unlock()
如何唤醒等待线程?- ReentrantLock 的
unlock()
→ 调用sync.release(1)
; - AQS 的
release(1)
→ 调用tryRelease
释放锁(重入计数减 1,直到state == 0
); - 锁彻底释放后 → 调用
unparkSuccessor
,从同步队列中找到最前面的有效节点,唤醒其对应的线程; - 被唤醒的线程 → 回到
acquireQueued
中,再次尝试抢锁(tryAcquire
)。
- ReentrantLock 的
3.3.9 tryRelease()
-
tryRelease
是 AQS 定义的抽象方法,由子类(如 ReentrantLock 的Sync
)重写,实现具体的释放锁逻辑;protected final boolean tryRelease(int releases) {// 1. 计算释放后的 state 值(state 是 AQS 的核心变量,代表“重入次数”)int c = getState() - releases; // 2. 校验:只有“持有锁的线程”才能释放锁if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException(); // 3. 标记“是否彻底释放锁”(state == 0 表示彻底释放)boolean free = false;if (c == 0) { // state 减到 0 → 彻底释放锁free = true;setExclusiveOwnerThread(null); // 清除“锁的持有者”标记}// 4. 更新 state 的值setState(c); // 5. 返回是否“彻底释放锁”return free; }
-
AQS 的
state
变量,在 ReentrantLock 中代表 重入次数:state = 0
:锁空闲;state > 0
:锁被占用(值 = 重入次数);
-
释放时的重入计数递减:每次
tryRelease
会让state = state - releases
(releases
通常为 1,对应一次unlock()
); -
持有锁线程校验:只有“当前线程 == 锁的持有者线程”,才能释放锁 → 否则抛
IllegalMonitorStateException
(防止其他线程非法释放锁); -
彻底释放的判断:当
state == 0
时,标记free = true
,并清除锁的持有者(setExclusiveOwnerThread(null)
)→ 表示锁完全空闲,其他线程可竞争; -
返回值的意义:
- 返回
true
:锁被彻底释放(state == 0
)→ AQS 会唤醒同步队列中的等待线程; - 返回
false
:锁只是减少了重入次数(state > 0
)→ 还没完全释放,不唤醒其他线程;
- 返回
-
-
ReentrantLock 的
unlock()
最终会调用 AQS 的release
方法,而release
内部会调用tryRelease
:-
若
tryRelease
返回true
(彻底释放锁),release
会唤醒同步队列中的等待线程; -
若返回
false
(只是减少重入次数),则不唤醒线程(锁还没完全释放)。
-