AQS 原理:同步队列与加锁解锁全流程
AQS(AbstractQueuedSynchronizer)是 Java 并发包的核心骨架,ReentrantLock、Semaphore、CountDownLatch 等都建立在它之上。本文以 JDK 8 源码为基础,从数据结构到加锁/解锁全流程逐层拆解。
目录
| 章节 | 说明 |
|---|---|
| AQS 是什么 | 定位与设计思想 |
| 核心数据结构 | Node、state、CLH 队列 |
| 模板方法:子类只需实现 5 个方法 | tryAcquire / tryRelease 等 |
| 加锁全流程 | acquire → addWaiter → acquireQueued |
| 解锁全流程 | release → unparkSuccessor |
| CANCELLED 节点的产生与清理 | cancelAcquire 详解 |
| 中断处理机制 | 协作式中断的设计 |
| 公平锁 vs 非公平锁的差异 | hasQueuedPredecessors |
| 可重入性实现 | state 累加与还原 |
| AQS 在 JUC 中的应用 | 各工具类与 state 的对应关系 |
| 自定义同步器示例 | 手写一个简单互斥锁 |
AQS 是什么
AQS 提供三项核心能力:
- 原子管理同步状态:用一个
volatile int state表示所有同步状态 - 阻塞/唤醒线程:依赖
LockSupport.park / unpark,不使用低效的Object.wait - FIFO 等待队列:CLH 变体的双向链表,管理竞争失败的线程
设计核心:子类只负责定义"state 的含义"和"如何修改 state",其余排队、阻塞、唤醒逻辑全由 AQS 框架完成。
AQS 分五层:
第 1 层(API 层):Lock.lock() / unlock() 等对外接口
第 2 层(锁获取):acquire() / release() —— AQS 框架实现
第 3 层(等待队列):addWaiter() / acquireQueued() —— AQS 框架实现
第 4 层(阻塞/唤醒):LockSupport.park / unpark
第 5 层(基础数据):volatile state、Node 双向链表、CAS 原语
核心数据结构
state 字段
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
protected final int getState() // volatile 读
protected final void setState(int newState) // volatile 写
protected final boolean compareAndSetState(int e, int u) // CAS 更新
state 的具体含义由子类定义:
| 实现类 | state 含义 |
|---|---|
ReentrantLock | 0 = 未锁;≥1 = 被持有次数(可重入计数) |
Semaphore | 当前剩余许可数 |
CountDownLatch | 剩余计数,归零后所有 await 解除 |
ReentrantReadWriteLock | 高 16 位 = 读锁计数,低 16 位 = 写锁计数 |
Node 节点
CLH 等待队列中每个节点对应一个等待线程:
// java.util.concurrent.locks.AbstractQueuedSynchronizer.Node
static final class Node {
// 线程等待锁的模式
static final Node SHARED = new Node(); // 共享模式(如读锁、Semaphore)
static final Node EXCLUSIVE = null; // 独占模式(如写锁、ReentrantLock)
// waitStatus 枚举值
static final int CANCELLED = 1; // 线程放弃等待(超时或中断)
static final int SIGNAL = -1; // 后继节点等待被唤醒
static final int CONDITION = -2; // 节点在条件队列中等待
static final int PROPAGATE = -3; // 共享模式下需要向后传播唤醒
volatile int waitStatus; // 节点状态,默认 0
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 该节点绑定的线程
Node nextWaiter; // 条件队列中的下一个节点(Condition Queue 专用)
}
| waitStatus | 值 | 含义 |
|---|---|---|
| 初始化 | 0 | 节点刚入队,尚未设置状态 |
SIGNAL | -1 | 前驱持有锁,我(后继)已挂起,释放时请唤醒我 |
CANCELLED | 1 | 放弃等待,需要从队列中清除 |
CONDITION | -2 | 在条件队列中等待(ConditionObject 专用) |
PROPAGATE | -3 | 共享模式下唤醒需要向后传播 |
为什么 waitStatus 记在前驱节点上,而不是自己身上? 因为"谁负责唤醒我,谁就记着这件事"——前驱释放锁时会检查自己的 waitStatus,看是否需要唤醒后继。如果记在自己节点上,前驱释放锁时还要去读后继的状态,多一次跨节点访问,且在后继节点还未完全初始化时可能有并发问题。
日常加锁/解锁流程里最核心的只有两个状态:0(刚入队)和 SIGNAL(已挂起,请唤醒我)。线程挂起前必须先把前驱的 waitStatus 设为 SIGNAL,这是"我要睡了,前驱你记得叫醒我"的协议。
CLH 变体双向队列结构
为什么不用普通队列
如果用普通队列实现锁等待,所有等待线程会盯着同一个变量(锁的 state)自旋:
线程1 ──┐
线程2 ──┤──▶ 轮询 state == 0?
线程3 ──┘
问题:锁释放时 state 被写入,所有等待线程的 Cache Line 全部失效,N 个线程同时涌来读新值——这叫 Cache Line 惊群(Thundering Herd),线程越多性能越差。
CLH 的解法:每个线程只盯自己的前驱
CLH 是一个虚拟链表,每个节点有 locked 标志,线程只监视自己前驱节点的状态:
[node1: locked=false] ← [node2: 盯 node1.locked] ← [node3: 盯 node2.locked]
↑ 当前持锁 ↑ 等待中 ↑ 等待中
锁释放时只把自己节点的 locked = false:
- 只有直接后继(node2)的 Cache Line 失效
- node3 完全不受影响,继续在本地 Cache 自旋
- N 个线程等待,释放锁只影响 1 个线程,彻底消除惊群
| 普通队列 | CLH 队列 | |
|---|---|---|
| 自旋目标 | 所有线程盯同一变量 | 每个线程盯自己前驱 |
| 释放锁时 Cache 失效范围 | 全部等待线程 | 仅直接后继 |
| NUMA 友好? | ❌ 跨核竞争 | ✅ 本地 Cache 自旋 |
| 公平性 | 不保证 | 天然 FIFO |
AQS 的变体:双向链表 + park 挂起
原始 CLH 是纯自旋,Java 线程纯自旋浪费 CPU,所以 AQS 做了两处改造:
- 单向 → 双向:加了
prev指针,方便跳过 CANCELLED 节点 - 纯自旋 → 自旋几次后 park 挂起:减少 CPU 空转
// AQS acquireQueued 简化逻辑
for (;;) {
if (前驱是 head && tryAcquire()) break; // 短暂自旋尝试
if (shouldPark) {
LockSupport.park(this); // 自旋失败,真正挂起
}
}
队列结构:
graph LR
H["Head(虚节点)<br/>thread=null<br/>waitStatus=-1"] --> A["Node-A<br/>Thread-2<br/>waitStatus=-1"]
A --> B["Node-B<br/>Thread-3<br/>waitStatus=-1"]
B --> T["Tail(Node-C)<br/>Thread-4<br/>waitStatus=0"]
T --> B
B --> A
A --> H
style H fill:#eee,stroke:#999
关键设计:头节点是虚节点(dummy node),不绑定任何线程,仅作占位。真正第一个等待线程在 head.next。
虚节点的真正作用:统一边界条件
虚节点不是为了解决并发安全问题(CAS 入队本身已保证),而是为了让第一个入队的真实线程也能满足 p == head 这个条件。
acquireQueued 的核心逻辑是:
if (p == head && tryAcquire(arg)) {
setHead(node); // 获锁成功,把自己升为新 head
...
}
只有前驱是 head 的线程,才有资格去 tryAcquire。 如果没有虚节点,第一个入队的线程自己就是 head,前驱为 null,p == head 永远不成立,第一个等待者会被饿死。有了虚节点,所有等待线程的逻辑完全一致,不需要对"我是不是队列里第一个"做特判。
Head 是动态游标,不是固定虚节点
Head 不是一个永远不变的占位节点,而是随着锁的获取不断向后推进:
初始: Head(虚节点)← → Node-A(Thread-2)← → Node-B(Thread-3)
Thread-2 获锁后:
setHead(Node-A) // Node-A 升为新 head
Node-A.thread = null // 清空 thread,变成新虚节点
原 Head 断开,被 GC 回收
变成: Head(原 Node-A, thread=null)← → Node-B(Thread-3)
获锁成功的节点会把自己"变成"虚节点——清空 thread 引用,成为下一轮的哨兵。Head 始终代表"当前持锁线程的占位",是一个不断前进的游标,而不是静态的哨兵。
模板方法:子类只需实现 5 个方法
// 子类需要覆盖的 5 个钩子方法(默认抛 UnsupportedOperationException)
protected boolean tryAcquire(int arg) // 独占:尝试获取锁
protected boolean tryRelease(int arg) // 独占:尝试释放锁
protected int tryAcquireShared(int arg) // 共享:负数=失败,0=成功无余量,正数=成功有余量
protected boolean tryReleaseShared(int arg) // 共享:释放后是否需要唤醒后继
protected boolean isHeldExclusively() // 当前线程是否独占持有(Condition 依赖)
ReentrantLock 是独占锁,只实现 tryAcquire + tryRelease,其余全由框架承包。
加锁全流程
以 非公平锁 ReentrantLock.lock() 为例,串联整个加锁链路。
第一步:lock() 入口
// java.util.concurrent.locks.ReentrantLock.NonfairSync
final void lock() {
// 快速路径:state=0 时直接 CAS 抢锁,不入队
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 快速路径失败,走 AQS 标准流程
}
非公平锁在入队前先"插队"抢一次,抢到直接返回,抢不到才走 acquire。
第二步:acquire()
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 再次尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 失败则入队并阻塞
selfInterrupt(); // 补偿中断标志
}
acquire 是一行逻辑密集的代码,拆解为三步:
tryAcquire(arg):再尝试一次(非公平锁会再插队)addWaiter(Node.EXCLUSIVE):失败则将当前线程包装为 Node 加入队列尾部acquireQueued(...):在队列中自旋等待,直到获取到锁,返回是否被中断过
第三步:addWaiter() — 入队
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 快速路径:CAS 将 node 设为新尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 队列为空或 CAS 失败 → 进入自旋初始化/入队
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// 队列未初始化:创建虚拟头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
注意:头节点的初始化用
new Node()(无参构造),是一个不绑定线程的虚节点。
入队过程可视化(线程 2、3 相继入队):
sequenceDiagram
participant T1 as Thread-1(持锁)
participant T2 as Thread-2
participant T3 as Thread-3
participant Q as CLH Queue
T2->>Q: addWaiter → enq → 初始化虚头节点 + 自身入队尾
Note over Q: Head(虚) ←→ Node(T2)
T3->>Q: addWaiter → CAS 入队尾
Note over Q: Head(虚) ←→ Node(T2) ←→ Node(T3)
第四步:acquireQueued() — 自旋等待
// java.util.concurrent.locks.AbstractQueuedSynchronizer
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取前驱节点
// 只有前驱是 head(即我是第一个等待者),才有资格尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功:自己成为新的虚头节点
p.next = null; // 原头节点断链,help GC
failed = false;
return interrupted;
}
// 获取失败:判断是否需要挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); // 异常退出时取消节点
}
}
setHead 把获取到锁的节点变为新虚头节点(清空 thread 和 prev),原头节点被 GC:
private void setHead(Node node) {
head = node;
node.thread = null; // 虚节点不持有线程引用
node.prev = null;
}
第五步:shouldParkAfterFailedAcquire() — 决定是否挂起
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱已设置 SIGNAL,我可以安心挂起,等前驱唤醒我
return true;
if (ws > 0) {
// 前驱是 CANCELLED(>0),跳过所有取消节点,找到有效前驱
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱状态为 0 或 PROPAGATE:CAS 将其设为 SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; // 本次不挂起,下一轮循环再检查
}
逻辑:只有确认前驱的
waitStatus == SIGNAL,当前线程才真正挂起。否则先把前驱设为 SIGNAL,下一次循环进来就会返回true并挂起。
flowchart TD
A["shouldParkAfterFailedAcquire<br/>前驱 waitStatus = ?"] --> B{ws == SIGNAL?}
B -->|是| C["return true<br/>(可以挂起)"]
B -->|ws > 0 取消| D["跳过取消节点<br/>重新链接前驱"]
D --> E["return false<br/>(重新循环)"]
B -->|ws == 0 或 PROPAGATE| F["CAS 设前驱为 SIGNAL"]
F --> E
style C fill:#cfc,stroke:#060
第六步:parkAndCheckInterrupt() — 真正挂起
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 挂起当前线程,直到被 unpark 或中断
return Thread.interrupted(); // 返回并清除中断标志
}
LockSupport.park 是无锁的线程挂起,比 Object.wait 更轻量,且不需要持有监视器锁。
为什么不复用 suspend/resume 或 wait/notify
JDK 1.5 之前有两套线程挂起机制,但都有根本性缺陷:
Thread.suspend() / resume()(已废弃):
- 致命问题:
suspend挂起时不释放锁。如果线程持有锁时被 suspend,其他所有等待该锁的线程永久死锁 - 通知丢失:
resume()先于suspend()执行,线程永久挂起
Object.wait() / notify():
- 必须持有 synchronized 锁才能调用,与锁强耦合,无法单独使用
- 同样有通知丢失问题,需要额外的标志位防范
- 设计粒度错误:
wait/notify是"对象级别"的概念,假设挂起和唤醒永远发生在同一把锁的上下文里;而 AQS 需要的是"挂起/唤醒某个特定线程",两者抽象层次不同,修补解决不了根本问题
LockSupport.park / unpark 的解法:
底层每个线程持有一个许可证(permit,值只有 0 和 1):
unpark(t):给线程 t 发一张许可证(最多积累 1 张)park():消费一张许可证;没有则挂起
关键:unpark 可以在 park 之前调用,许可证会被保留,下次 park 直接消费不阻塞——彻底解决通知丢失问题,且完全不依赖任何锁。
| suspend/resume | wait/notify | park/unpark | |
|---|---|---|---|
| 需要持锁? | ❌ | ✅ 必须 | ❌ |
| 挂起时释放锁? | ❌ 不释放 | ✅ 释放 | — (无锁概念) |
| 通知可以提前? | ❌ 丢失 | ❌ 丢失 | ✅ 许可证保留 |
| 与锁耦合? | ❌ | ✅ 强耦合 | ❌ 完全解耦 |
解锁全流程
unlock() → release() → tryRelease() → unparkSuccessor()
// ReentrantLock
public void unlock() {
sync.release(1);
}
// AQS
public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类释放 state
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒队列中下一个等待线程
return true;
}
return false;
}
// ReentrantLock.Sync(公平/非公平共用)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null); // 清空持有线程
}
setState(c); // 写 volatile,建立 Happens-Before
return free; // 只有完全释放(c==0)才返回 true
}
release 中 h.waitStatus != 0 的三种情况:
| 条件 | 含义 | 动作 |
|---|---|---|
h == null | 队列未初始化,无竞争 | 不唤醒 |
h != null && waitStatus == 0 | 后继线程还在运行,尚未挂起 | 不唤醒 |
h != null && waitStatus < 0 | 后继线程已挂起,等待唤醒 | 唤醒 |
unparkSuccessor() — 找到并唤醒后继
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 清除头节点状态
Node s = node.next;
// 如果 next 为 null 或已取消,从尾部往前找第一个有效节点
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; // 一直找到最靠近 head 的有效节点
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒
}
为什么从尾部往前找,而不是从前往后?
入队操作不是原子的,addWaiter 中有这样的顺序:
node.prev = pred; // ① prev 先建立
compareAndSetTail(...); // ② tail 指针原子更新
pred.next = node; // ③ next 最后建立(可能还没完成)
若在 ② 和 ③ 之间执行 unparkSuccessor,从前往后遍历会因 pred.next 尚未赋值而漏掉新节点。而 prev 在 ① 已经可靠建立,从尾部往前遍历则不会遗漏。
CANCELLED 节点的产生与清理
节点在以下情况进入 CANCELLED 状态:acquireQueued 的 finally 块中,若 failed == true(通常由超时或中断引发异常),调用 cancelAcquire:
private void cancelAcquire(Node node) {
if (node == null) return;
node.thread = null; // 解除线程引用
// 跳过前面所有已取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED; // 标记自己为取消
// 分三种情况处理
if (node == tail && compareAndSetTail(node, pred)) {
// 情况 1:我是尾节点 → 直接把前驱设为新尾节点
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head
&& ((ws = pred.waitStatus) == Node.SIGNAL
|| (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
&& pred.thread != null) {
// 情况 2:中间节点 → 让前驱的 next 跳过我,指向我的后继
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 情况 3:我是 head 的直接后继 → 唤醒我的后继,让它重新竞争
unparkSuccessor(node);
}
node.next = node; // help GC(自引用)
}
}
三种情况图示:
graph LR
subgraph "情况1:取消节点是尾节点"
H1["Head"] --> A1["Node-A"] --> X1["Node-X<br/>CANCELLED"]
H1 -. "新 tail" .-> A1
end
subgraph "情况2:取消节点在中间"
H2["Head"] --> A2["Node-A"] --> X2["Node-X<br/>CANCELLED"] --> B2["Node-B"]
A2 -. "跳过 X" .-> B2
end
subgraph "情况3:取消节点是 head 直接后继"
H3["Head"] --> X3["Node-X<br/>CANCELLED"] --> B3["Node-B"]
X3 -. "唤醒 B" .-> B3
end
为什么只改 next,不改 prev?
cancelAcquire执行时,当前节点的前驱可能正在shouldParkAfterFailedAcquire里操作,修改 prev 不安全。prev的清理由shouldParkAfterFailedAcquire中的跳过取消节点逻辑负责(那段代码执行时锁已被占有,前驱不会再变化,安全)。
中断处理机制
AQS 对中断采用协作式处理:不立即响应,而是记录下来,获取锁后补偿。
// acquireQueued 内部
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true; // 记录"曾经被中断过"
// 最终拿到锁后返回 interrupted
return interrupted;
// acquire 收到 true,调用 selfInterrupt 补偿
static void selfInterrupt() {
Thread.currentThread().interrupt(); // 重新设置中断标志
}
完整流程:
- 线程在
park中被中断唤醒 Thread.interrupted()返回true并清除中断标志interrupted = true记录- 继续自旋,直到获取到锁
acquire收到interrupted=true,调用selfInterrupt()重新设置中断位- 调用方可以通过检查
Thread.currentThread().isInterrupted()得知曾被中断
公平锁 vs 非公平锁的差异
两者 tryAcquire 的唯一区别是是否检查队列中有前驱:
// 非公平锁(NonfairSync)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
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;
}
// 公平锁(FairSync)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 多了这个判断:队列为空或我是第一个,才能抢
if (!hasQueuedPredecessors() && 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:返回 true 表示有其他线程在队列中排在前面
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// h != t 说明队列有节点;h.next == null 说明正在初始化;
// h.next.thread != current 说明排在我前面的是别人
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
| 维度 | 非公平锁 | 公平锁 |
|---|---|---|
| 吞吐量 | 高(减少上下文切换) | 低 |
| 等待时间 | 不均匀,可能饥饿 | 均匀,FIFO 顺序 |
| 适用场景 | 高并发、锁持有时间短 | 任务等待时间敏感 |
可重入性实现
ReentrantLock 的可重入靠 state 累加实现:
// 加锁:同一线程每重入一次,state +1
if (c == 0) {
// 首次获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 同一线程重入:state 累加
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 解锁:每次 -1,直到 0 才真正释放
int c = getState() - releases;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free; // c==0 时返回 true,触发唤醒后继
AQS 在 JUC 中的应用
| 同步工具 | state 含义 | 模式 |
|---|---|---|
ReentrantLock | 重入次数(0=无锁) | 独占 |
ReentrantReadWriteLock | 高 16 位=读锁次数,低 16 位=写锁次数 | 独占 + 共享 |
Semaphore | 剩余许可数 | 共享 |
CountDownLatch | 剩余计数(归零后所有等待解除) | 共享 |
ThreadPoolExecutor.Worker | 0=空闲,1=运行(防止中断运行中 Worker) | 独占 |
自定义同步器示例
只需继承 AQS,实现 tryAcquire 和 tryRelease,就能得到一个完整的互斥锁:
public class SimpleMutex {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// state: 0=未锁,1=已锁
return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease(int arg) {
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
}
使用效果(两线程各加 10000 次,结果精确为 20000):
SimpleMutex mutex = new SimpleMutex();
int[] count = {0};
Runnable task = () -> {
mutex.lock();
try {
for (int i = 0; i < 10000; i++) count[0]++;
} finally {
mutex.unlock();
}
};
Thread t1 = new Thread(task), t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(count[0]); // 始终输出 20000
参考资料
- Java 8 AQS 官方文档
- Lea D. The java.util.concurrent synchronizer framework. Science of Computer Programming, 2005
- 《Java 并发编程实战》(Brian Goetz 等著)
评论 (0)