Java并发之AQS详解
- Java并发之AQS详解
- 1. AQS 是什么?
- 2. 核心原理
- 3. 工作流程(以 ReentrantLock 的独占模式为例)
- 4. 主要同步器实现
- 5. Synchronized 、Lock、Condition、AQS
- 对比:synchronized + wait()、Lock + await()
- 6. 总结与要点
- AbstractQueuedSynchronizer的方法
- AQS使用的三种模式
Java并发之AQS详解
Java并发之AQS详解 - waterystone - 博客园
1. AQS 是什么?
AQS,全称 AbstractQueuedSynchronizer(抽象队列同步器),是 Java 并发包 java.util.concurrent.locks 下的一个核心基础框架。
它的主要作用是为构建锁和同步器(如 Semaphore、CountDownLatch 等)提供一个底层的、通用的同步机制。你可以把它想象成一个“同步器的骨架”,它帮你处理了复杂的线程排队、阻塞、唤醒等底层细节,你只需要按需实现一些关键的方法,就能定制出自己的同步工具。
核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
一句话总结:AQS 是 JUC 锁的“大脑”,它管理着线程的排队、阻塞和唤醒。
2. 核心原理
AQS 的核心原理可以概括为三部分:一个状态、一个队列 和 一套模板方法。
2.1 一个状态:state
AQS 内部维护了一个关键的 volatile 整型变量,名为 state。
- 作用:
state表示共享资源的状态。具体含义由子类定义,非常灵活。 - 示例:
- 在
ReentrantLock中,state=0表示锁空闲,state=1表示锁被占用,state>1表示锁被同一个线程重入。 - 在
Semaphore中,state表示可用的许可证数量。 - 在
CountDownLatch中,state表示倒计数的数值。
- 在
对 state 的修改使用 CAS(Compare-And-Swap)操作来保证原子性,这是实现非阻塞同步的基础。
2.2 一个队列:CLH 变种队列
AQS 内部维护了一个双向的 FIFO 线程等待队列(通常被称为 CLH 队列的变种)。当线程获取资源失败时,AQS 会将该线程以及其等待状态(如是否独占)包装成一个 Node 节点,并将其加入到队列尾部,然后阻塞该线程。
- Node 节点:包含了线程引用、等待状态(如
CANCELLED,SIGNAL等)、前驱节点(prev)和后继节点(next)。 - 作用:这个队列是所有未能立即获取到资源的线程的“等候室”,它严格保证了等待的公平性(FIFO)。
2.3 一套模板方法:获取与释放
AQS 使用了 模板方法设计模式。它定义了一套顶层的获取和释放资源的流程(如 acquire(int arg) 和 release(int arg)),而将一些关键的是否成功、如何修改状态的判断留给子类去实现。
子类需要重写的关键方法:
protected boolean tryAcquire(int arg):尝试以独占方式获取资源。成功则返回 true,失败则返回 false。protected boolean tryRelease(int arg):尝试以独占方式释放资源。成功则返回 true,失败则返回 false。protected int tryAcquireShared(int arg):尝试以共享方式获取资源。负数表示失败;0 表示成功,但无剩余可用资源;正数表示成功,且有剩余资源。protected boolean tryReleaseShared(int arg):尝试以共享方式释放资源。
AQS 提供的核心模板方法(供使用者调用):
- 独占模式:
acquire(int arg):获取资源,如果失败则进入队列等待。此过程不可中断。release(int arg):释放资源,成功后唤醒队列中下一个等待的线程。
- 共享模式:
acquireShared(int arg)releaseShared(int arg)
3. 工作流程(以 ReentrantLock 的独占模式为例)
我们以锁的获取和释放来看 AQS 是如何工作的。
3.1 获取锁 (lock() -> acquire(1))
- 线程 A 调用
lock()。 lock()内部会调用 AQS 的acquire(1)。acquire(1)的流程如下:tryAcquire(1):子类(ReentrantLock 的 Sync)实现此方法。检查state:- 如果
state == 0(锁空闲),则通过 CAS 将其设为 1,并设置当前线程为独占线程,返回true。流程结束,线程 A 获得锁。 - 如果
state != 0,但独占线程就是线程 A 自己(锁重入),则将state加 1,返回true。 - 否则,返回
false。
- 如果
- 如果
tryAcquire返回false(获取失败),则调用addWaiter(Node.EXCLUSIVE)。将线程 A 包装成一个独占模式的 Node 节点,并采用 CAS 方式安全地插入到等待队列的尾部。 - 接着调用
acquireQueued(...)。这个方法是核心中的核心:- 它会让节点自旋地尝试获取锁(在它即将成为队首时)。
- 如果还是失败,则会判断是否应该阻塞自己(通过前驱节点的
waitStatus)。 - 最终,通过
LockSupport.park()将线程 A 挂起(阻塞)。
3.2 释放锁 (unlock() -> release(1))
- 线程 A 调用
unlock()。 unlock()内部会调用 AQS 的release(1)。release(1)的流程如下:tryRelease(1):子类实现此方法。将state减 1。如果state减到 0,表示锁完全释放,清空独占线程,返回true。- 如果
tryRelease返回true,则它会找到等待队列中的头节点(head)。 - 如果头节点不为空且其状态有效,则调用
unparkSuccessor(Node node)。 - 这个方法会使用
LockSupport.unpark(thread)唤醒 头节点后继节点中第一个未被取消的线程(假设是线程 B)。
- 线程 B 被唤醒后,会从之前在
acquireQueued中被park()的地方继续执行。 - 线程 B 会再次自旋尝试
tryAcquire。此时锁已被线程 A 释放,所以线程 B 有很大概率成功获取到锁,然后将自己设置为新的头节点,继续执行。
4. 主要同步器实现
AQS 是 JUC 中众多同步工具的基础:
- ReentrantLock:独占锁,使用 AQS 的独占模式。
- ReentrantReadWriteLock:读写锁。其读锁使用共享模式,写锁使用独占模式。
- Semaphore:信号量,使用 AQS 的共享模式。
- CountDownLatch:倒计时器,使用 AQS 的共享模式。
state初始化为计数。 - CyclicBarrier:循环栅栏(其底层使用了 ReentrantLock 和 Condition,而 Condition 的实现也依赖于 AQS)。
- ThreadPoolExecutor:线程池中的 Worker 类(工作线程)也使用了 AQS 来实现独占锁,用于判断线程是否空闲。
5. Synchronized 、Lock、Condition、AQS
AQS 使用 CAS + volatile state + CLH队列,比传统的 synchronized 有更好的并发性能。
- ✅
Lock替代原始的synchronized - ✅
Condition替代原始的wait/notify - ✅ 主流的
Lock和Condition实现都是基于 AQS
synchronized 和 Lock (基于AQS)
原始方式 (JDK 1.0+) 现代方式 (JDK 5+) synchronized → Lock (基于AQS) wait/notify → Condition (基于AQS)
// 原始的 wait/notify
synchronized (lock) {
while (!condition) {
lock.wait(); // 所有等待者混在一起
}
// ...
lock.notifyAll(); // 唤醒所有等待者
}
// 现代的 Condition
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!condition) {
condition.await(); // 只等待特定条件
}
// ...
condition.signal(); // 只唤醒一个等待此条件的线程
} finally {
lock.unlock();
}
// 现代的 Condition Lock 提供更多功能
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取锁,带超时
try {
// ...
} finally {
lock.unlock();
}
}
// 锁中断
lock.lockInterruptibly(); // 可中断的锁获取
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列未满条件
Condition notEmpty = lock.newCondition(); // 队列非空条件
// 生产者只等待 notFull 条件
// 消费者只等待 notEmpty 条件
// 互不干扰!
对比:synchronized + wait()、Lock + await()
Synchronized+wait()很多步骤都是底层处理了,加锁解锁等 。Lock+await()基本都是java代码处理,程序员可以看到,加锁解锁都可以看到状态变化
| 特性 | synchronized + wait() | Lock + await() |
|---|---|---|
| 可见性 | 🚨 黑盒操作 | 🎯 白盒可见 |
| 加锁 | 编译器生成字节码 | Java 代码实现 |
| 释放锁 | JVM 底层处理 | AQS 状态管理 |
| 状态跟踪 | 困难 | 容易 |
| 调试 | 困难 | 友好 |
| 灵活性 | 有限 | 丰富 |
设计哲学:
- synchronized: "别担心,我来处理" - 面向简单使用
- Lock: "你自己控制" - 面向高级需求
这就是为什么在复杂的并发系统中,Lock 通常更受青睐 - 可见性和可控性更重要!
6. 总结与要点
- 定位:AQS 是构建锁和同步器的框架,不是直接给业务开发者使用的类。
- 核心机制:通过一个
volatile的state表示状态,一个 FIFO 队列管理等待线程,一套 CAS 操作保证状态更新的原子性。 - 设计模式:模板方法模式。使用者继承 AQS 并重写指定方法,将 AQS 组合在自定义同步组件的实现中。
- 两种模式:独占模式(一次只有一个线程能执行,如 ReentrantLock)和共享模式(多个线程可同时执行,如 Semaphore/CountDownLatch)。
- 重要性:理解了 AQS,就理解了 JUC 包中大部分同步工具的实现原理,是 Java 并发编程进阶的必经之路。
通过 AQS,Java 提供了一种高效、安全且可扩展的方式来构建复杂的同步结构,极大地简化了并发编程的难度。
AbstractQueuedSynchronizer的方法
AQS要解决的问题:线程间同步协作的通用逻辑(排队、阻塞、唤醒)。“如何让多个线程按照既定规则有序地访问共享资源?”
AQS 的核心思想是:
- 一个 volatile 的 int 类型状态变量(state):表示共享资源的状态。
- 一个内置的 FIFO 双向队列(CLH队列的变体):用于管理所有获取资源失败的线程。
AQS 将方法分为两类:
- 面向使用者的模板方法:这些是
public final方法,使用者直接调用,如acquire,release。它们定义了同步器的基本工作流程。 - 需要子类重写的钩子方法:这些是
protected方法,由同步器的实现者(如ReentrantLock的作者)根据具体的同步需求(如独占锁、共享锁)来重写。
一、 面向使用者的模板方法 (public final)
这些方法是给锁和同步器使用者调用的。
独占模式 (Exclusive Mode)
一次只有一个线程能成功获取资源。
| 方法名 | 作用 |
|---|---|
acquire(int arg) |
核心获取方法。尝试获取资源,如果成功则返回,否则线程会进入等待队列,直到成功获取为止。此过程对中断不敏感。 |
acquireInterruptibly(int arg) |
与 acquire 类似,但会响应中断。如果在等待过程中被中断,会抛出 InterruptedException。 |
tryAcquireNanos(int arg, long nanosTimeout) |
在 acquireInterruptibly 的基础上增加了超时限制。如果在指定超时时间内未获取到资源,则返回 false;如果在等待中被中断,则抛出 InterruptedException。 |
release(int arg) |
核心释放方法。释放指定量的资源。成功释放后,它会唤醒等待队列中的下一个线程。 |
共享模式 (Shared Mode)
多个线程可以同时成功获取资源。
| 方法名 | 作用 |
|---|---|
acquireShared(int arg) |
共享模式下的核心获取方法。尝试获取共享资源。如果失败则进入队列等待,直到成功获取为止。对中断不敏感。 |
acquireSharedInterruptibly(int arg) |
共享模式下可响应中断的获取方法。 |
tryAcquireSharedNanos(int arg, long nanosTimeout) |
共享模式下带超时和中断响应的获取方法。 |
releaseShared(int arg) |
共享模式下的核心释放方法。释放指定量的共享资源。 |
状态查询方法
| 方法名 | 作用 |
|---|---|
getState() |
获取当前同步状态 state 的值。 |
setState(int newState) |
设置同步状态 state 的值。 |
compareAndSetState(int expect, int update) |
使用 CAS 操作原子性地更新 state 的值。这是实现无锁同步的关键。 |
hasQueuedThreads() |
查询是否有线程正在等待获取资源。 |
getQueueLength() |
获取等待队列的大致长度。 |
getQueuedThreads() |
返回一个包含等待队列中所有线程的集合。 |
isHeldExclusively() |
查询当前线程是否独占着资源(通常用于 Condition 的实现)。 |
二、 需要子类重写的钩子方法 (protected)
这些方法是 AQS 的“灵魂”,AQS 的模板方法会调用它们,但它们的默认实现通常是抛出 UnsupportedOperationException。同步器的实现者必须根据需求重写这些方法。
独占模式钩子方法
| 方法名 | 作用 |
|---|---|
tryAcquire(int arg) |
尝试以独占方式获取资源。 • 成功返回 true • 失败返回 false 实现者需要查询 state 并根据自己的逻辑判断当前线程是否能获取资源。 |
tryRelease(int arg) |
尝试以独占方式释放资源。 • 成功返回 true • 失败返回 false 实现者需要修改 state,并判断资源是否已完全释放,以便唤醒后续线程。 |
共享模式钩子方法
| 方法名 | 作用 |
|---|---|
tryAcquireShared(int arg) |
尝试以共享方式获取资源。 • 成功返回一个大于等于 0 的值(表示剩余资源量) • 失败返回一个负数 实现者需要判断当前是否有足够的共享资源可供获取。 |
tryReleaseShared(int arg) |
尝试以共享方式释放资源。 • 成功返回 true • 失败返回 false 实现者需要修改 state,并判断资源释放后,其他等待的共享线程是否可以被唤醒。 |
三、 工作流程示例
我们以 独占锁(如 ReentrantLock)为例,看一下 acquire 和 release 的流程:
acquire(int arg) 流程:
- 调用子类重写的
tryAcquire(arg)尝试直接获取资源。 - 如果成功,方法结束。
- 如果失败,AQS 会将当前线程包装成一个节点(Node)并加入等待队列尾部。
- 然后,在队列中自旋,检查自己的前驱节点是否是头节点(即下一个该轮到自己了)。
- 如果是头节点,则再次调用
tryAcquire(arg)尝试获取。 - 如果成功,将自己设为新的头节点,并退出。
- 如果失败或还不是头节点,则可能挂起线程,等待被前驱节点唤醒。
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 步骤1 & 2
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 步骤3 & 4 & 5 & 6 & 7
selfInterrupt(); // 在等待过程中如果被中断过,补上中断标记
}
release(int arg) 流程:
- 调用子类重写的
tryRelease(arg)尝试释放资源。 - 如果释放成功(例如,
state变为 0),则检查队列中是否有等待的线程。 - 如果有,则唤醒头节点的后继节点。
public final boolean release(int arg) {
if (tryRelease(arg)) { // 步骤1 & 2
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 步骤3:唤醒后继线程
return true;
}
return false;
}
总结
- 模板方法 (
acquire,release等):定义了同步的骨架,实现了线程排队、阻塞、唤醒等复杂逻辑。使用者直接调用它们。 - 钩子方法 (
tryAcquire,tryRelease等):定义了同步的策略,即“如何判断获取/释放是否成功”。由同步器的实现者重写。
通过这种“模板方法”设计模式,AQS 将复杂的同步器通用逻辑(队列管理、线程阻塞/唤醒)与特定于实现的逻辑(资源状态的判断)分离开,极大地简化了自定义同步器的开发。你只需要决定你的同步器是独占模式还是共享模式,然后重写相应的 tryXXX 方法即可。
| 场景 | 建议 | 例子 |
|---|---|---|
| 简单的互斥锁 | 只实现 tryAcquire/tryRelease(独占) |
ReentrantLock |
| 简单的资源池/信号量 | 只实现 tryAcquireShared/tryReleaseShared(共享) |
Semaphore,CountDownLatch |
| 复杂的读写同步 | 可以像 ReentrantReadWriteLock 那样精心设计,在同一个AQS中支持两种模式 |
ReentrantReadWriteLock |
独占模式 (Exclusive Mode)和共享模式 (Shared Mode)的区别
独占模式 (Exclusive Mode) 和共享模式 (Shared Mode) 最根本的区别在于:同一时间,能否有多个线程成功获取到同步资源。
类比说明
| 模式 | 经典例子 | 核心思想 |
|---|---|---|
| 独占模式 | 厕所 | 一个厕所(资源)一次只能被一个人(线程)占用。其他人必须排队等待前一个人出来。 |
| 共享模式 | 停车场 | 一个停车场(资源)有多个车位(state 值)。只要还有空车位(state > 0),多辆车(线程)就可以同时进入。 |
详细对比
| 特征 | 独占模式 (Exclusive) | 共享模式 (Shared) |
|---|---|---|
| 核心区别 | 排他的,一次只有一个线程能成功获取资源。 | 共享的,多个线程可以同时成功获取资源。 |
| 应用场景 | 实现互斥锁,如 ReentrantLock。 |
实现信号量、资源池,或同时允许的并发数,如 Semaphore, CountDownLatch。 |
AQS 状态 state |
通常表示锁的重入次数(如 ReentrantLock)。0 表示空闲,1 表示被持有,>1 表示被同一线程重入。 |
通常表示可用资源的数量(如 Semaphore)。比如 state=10 表示有 10 个许可可用。 |
| 节点类型 | 等待队列中的节点标记为 Node.EXCLUSIVE。 |
等待队列中的节点标记为 Node.SHARED。 |
| 获取资源 | tryAcquire(int arg):成功返回 true,失败返回 false。 |
tryAcquireShared(int arg):成功返回剩余资源数(>=0),失败返回负数。 |
| 释放资源 | tryRelease(int arg):成功返回 true,失败返回 false。 |
tryReleaseShared(int arg):成功返回 true,失败返回 false。 |
| 传播行为 | 当一个线程释放资源时,只会唤醒等待队列中的下一个线程(头节点的后继节点)。 | 当一个线程释放资源时,它可能会唤醒后续一连串的共享模式节点,直到遇到一个独占模式节点为止(“传播”特性)。 |
工作流程细节对比
1. 获取资源 (acquire vs acquireShared)
独占模式 (acquire):
- 调用
tryAcquire尝试获取。 - 失败后,将线程包装成 EXCLUSIVE 节点加入队尾。
- 当前驱节点是头节点时,再次尝试
tryAcquire。 - 成功后,将自己设为新头节点,流程结束。
共享模式 (acquireShared):
- 调用
tryAcquireShared尝试获取。 - 如果返回值 >= 0(获取成功且有剩余资源),流程结束。
- 如果返回值 < 0(获取失败),将线程包装成 SHARED 节点加入队尾。
- 当前驱节点是头节点时,再次尝试
tryAcquireShared。 - 如果成功且返回值 >= 0,不仅将自己设为新头节点,还会调用
doReleaseShared()尝试唤醒后续的共享节点(这就是“传播”)。
2. 释放资源 (release vs releaseShared)
独占模式 (release):
- 调用
tryRelease,如果返回true(表示资源完全释放)。 - 检查队列,唤醒头节点的下一个节点(如果存在)。
共享模式 (releaseShared):
- 调用
tryReleaseShared,如果返回true(表示资源释放成功)。 - 调用
doReleaseShared(),它会持续性地唤醒节点,以实现传播。它不仅仅唤醒一个,而是会检查并确保所有可被唤醒的共享节点都被唤醒。
代码示例对比
独占模式 (ReentrantLock):
- 线程 A 调用
lock()->tryAcquire(1)成功,将state从 0 改为 1。线程 A 获取锁。 - 线程 B 调用
lock()->tryAcquire(1)失败(因为state != 0),线程 B 进入队列等待。 - 线程 C 调用
lock()->tryAcquire(1)失败,线程 C 进入队列等待。 - 线程 A 调用
unlock()->tryRelease(1)成功,将state改回 0,然后唤醒队列中的线程 B。
共享模式 (Semaphore(2)):
- 线程 A 调用
acquire()->tryAcquireShared(1)成功,返回 1(剩余许可数)。state从 2 减为 1。线程 A 获取许可。 - 线程 B 调用
acquire()->tryAcquireShared(1)成功,返回 0(剩余许可数)。state从 1 减为 0。线程 B 获取许可。 - 线程 C 调用
acquire()->tryAcquireShared(1)失败,返回 -1(因为state == 0),线程 C 进入队列等待。 - 线程 A 调用
release()->tryReleaseShared(1)成功,将state从 0 加为 1。然后它不仅会唤醒线程 C,还会因为“传播”机制,可能让线程 C 之后的其他共享节点也被唤醒(如果资源足够)。线程 C 被唤醒后成功获取许可。
独占模式和共享模式的实现
场景:银行账户操作
需求:
- 多个线程可以同时查询账户余额(共享读)
- 但一次只能有一个线程修改账户余额(独占写)
- 当有线程在修改时,其他查询和修改线程都必须等待
1. 独占模式实现(简单的互斥锁)
这个实现只保证互斥,不区分读写操作。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
/**
* 使用独占模式实现的简单银行账户锁
* 不区分读写,所有操作都互斥
*/
class ExclusiveBankAccount {
private final Sync sync = new Sync();
private int balance;
public ExclusiveBankAccount(int initialBalance) {
this.balance = initialBalance;
}
// 独占模式AQS实现
private static class Sync extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override
protected boolean tryAcquire(int acquires) {
// 用CAS将state从0改为1,成功则获取锁
return compareAndSetState(0, 1);
}
// 尝试释放锁
@Override
protected boolean tryRelease(int releases) {
// 将state从1改回0
setState(0);
return true;
}
}
// 查询余额 - 也需要获取锁
public int getBalance() {
sync.acquire(1); // 获取锁
try {
System.out.println(Thread.currentThread().getName() + " 查询余额: " + balance);
return balance;
} finally {
sync.release(1); // 释放锁
}
}
// 修改余额
public void transfer(int amount) {
sync.acquire(1); // 获取锁
try {
int oldBalance = balance;
balance += amount;
System.out.println(Thread.currentThread().getName() + " 修改余额: " + oldBalance + " -> " + balance);
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
sync.release(1); // 释放锁
}
}
}
2. 共享模式实现(读写锁语义)
这个实现允许多个读操作并发,但写操作独占。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
/**
* 使用共享模式实现的读写锁银行账户
* 允许多个读操作并发,写操作独占
*/
class SharedBankAccount {
private final Sync sync = new Sync();
private int balance;
public SharedBankAccount(int initialBalance) {
this.balance = initialBalance;
}
// 共享模式AQS实现 - 读写锁语义
private static class Sync extends AbstractQueuedSynchronizer {
// 共享获取 - 读锁
@Override
protected int tryAcquireShared(int acquires) {
for (;;) {
int state = getState();
// 如果有写锁持有(state为-1),获取读锁失败
if (state < 0) {
return -1;
}
// 尝试增加读锁计数
if (compareAndSetState(state, state + 1)) {
return 1; // 成功,返回正数表示还有剩余资源
}
}
}
// 共享释放 - 读锁释放
@Override
protected boolean tryReleaseShared(int releases) {
for (;;) {
int state = getState();
if (compareAndSetState(state, state - 1)) {
return true;
}
}
}
// 独占获取 - 写锁
@Override
protected boolean tryAcquire(int acquires) {
// 只有当没有任何读锁和写锁时(state == 0)才能获取写锁
return compareAndSetState(0, -1);
}
// 独占释放 - 写锁释放
@Override
protected boolean tryRelease(int releases) {
// 将state从-1改回0
setState(0);
return true;
}
}
// 查询余额 - 使用共享锁(读锁)
public int getBalance() {
sync.acquireShared(1); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " 查询余额: " + balance);
Thread.sleep(50); // 模拟查询耗时
return balance;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
} finally {
sync.releaseShared(1); // 释放读锁
}
}
// 修改余额 - 使用独占锁(写锁)
public void transfer(int amount) {
sync.acquire(1); // 获取写锁
try {
int oldBalance = balance;
balance += amount;
System.out.println(Thread.currentThread().getName() + " 修改余额: " + oldBalance + " -> " + balance);
Thread.sleep(100); // 模拟修改耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
sync.release(1); // 释放写锁
}
}
}
3. 测试代码
public class BankAccountDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 独占模式测试(所有操作串行)===");
testExclusiveMode();
Thread.sleep(1000);
System.out.println("\n=== 共享模式测试(读操作并行,写操作串行)===");
testSharedMode();
}
private static void testExclusiveMode() throws InterruptedException {
ExclusiveBankAccount account = new ExclusiveBankAccount(1000);
// 创建多个线程同时进行查询和转账
Thread[] threads = new Thread[4];
for (int i = 0; i < threads.length; i++) {
final int index = i;
threads[i] = new Thread(() -> {
if (index % 2 == 0) {
account.getBalance(); // 查询操作
} else {
account.transfer(100); // 转账操作
}
}, "独占线程-" + i);
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
}
private static void testSharedMode() throws InterruptedException {
SharedBankAccount account = new SharedBankAccount(1000);
// 创建多个线程同时进行查询和转账
Thread[] threads = new Thread[6];
for (int i = 0; i < threads.length; i++) {
final int index = i;
threads[i] = new Thread(() -> {
if (index % 3 != 0) {
account.getBalance(); // 查询操作(共享)
} else {
account.transfer(100); // 转账操作(独占)
}
}, "共享线程-" + i);
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
}
}
运行结果分析
独占模式输出:
=== 独占模式测试(所有操作串行)===
独占线程-0 查询余额: 1000
独占线程-1 修改余额: 1000 -> 1100
独占线程-2 查询余额: 1100
独占线程-3 修改余额: 1100 -> 1200
所有操作都是串行的,即使查询操作也要等待。
共享模式输出:
=== 共享模式测试(读操作并行,写操作串行)===
共享线程-1 查询余额: 1000
共享线程-2 查询余额: 1000
共享线程-0 修改余额: 1000 -> 1100
共享线程-4 查询余额: 1100
共享线程-5 查询余额: 1100
共享线程-3 修改余额: 1100 -> 1200
可以看到多个查询操作可以同时进行(时间戳会很接近),但修改操作是串行的。
关键区别总结
| 方面 | 独占模式实现 | 共享模式实现 |
|---|---|---|
| 并发性 | 所有操作串行 | 读操作并行,写操作串行 |
| 性能 | 较低 | 较高(读多写少场景) |
| 实现复杂度 | 简单 | 复杂(需要处理读写互斥) |
| 适用场景 | 简单的互斥保护 | 读多写少的并发场景 |
| 当前jdk已有的实现 | ReentrantLock(独占模式) | ReentrantReadWriteLock(共享模式:ReadLock、WriteLock) |
这个例子完美展示了:
- 独占模式:用于实现简单的互斥锁
- 共享模式:用于实现更复杂的同步语义(如读写锁),提供更好的并发性能
问题
为什么
tryAcquireShared和tryReleaseShared实现中要for (;;) {}循环?包括jdk自己实现的StampedLock、CountDownLatch、ReentrantReadWriteLock都有。解答:
tryAcquireShared和tryReleaseShared实现中的for (;;) {}循环是实现无锁编程和解决竞态条件的关键。核心原因:CAS操作的失败重试机制
- CAS(Compare-And-Swap)是原子操作,但可能失败:
// CAS操作:只有当当前值等于expect时才更新为update // 如果失败,说明有其他线程修改了值 compareAndSetState(expect, update)没有循环的版本(有问题):
protected int tryAcquireShared(int acquires) { int state = getState(); if (state < 0) { // 有写锁 return -1; } // 问题:这里getState()和CAS之间,state可能已被其他线程修改! if (compareAndSetState(state, state + 1)) { return 1; } return -1; // CAS失败就返回失败,不合理! }有循环的正确版本:
protected int tryAcquireShared(int acquires) { for (;;) { // 无限循环,直到成功或明确失败 int state = getState(); if (state < 0) { return -1; // 明确失败条件:有写锁 } // CAS失败就继续循环重试 if (compareAndSetState(state, state + 1)) { return 1; // 成功 } // CAS失败时,循环继续,重新读取state并重试 } }总结:
for (;;) {}循环在共享模式中的必要性:
- 解决竞态条件:在读取状态和CAS更新之间,状态可能被其他线程修改
- 实现无锁算法:通过重试而不是阻塞来实现线程安全
- 保证正确性:确保在并发环境下资源计数的准确性
- 提高性能:避免使用重量级锁,在高度竞争时通过自旋重试
这种模式是乐观锁的典型实现:先读取,计算新值,然后尝试更新,如果失败就重试。这在读多写少的高并发场景中性能很好。
相比之下,独占模式通常更简单,因为状态变化是二元的(0→1或1→0),不需要复杂的条件检查和资源计算。
AQS使用的三种模式
// AQS使用的三种模式
// 直接CAS ≠ 锁(缺少排队):成功了就直接执行,失败就放弃。直接操作状态。
// 只调用tryAcquire ≠ 锁(还是缺少排队):成功了就直接执行,失败就放弃。虽然用了AQS方法,但没有使用acquire()的排队功能。
// 调用lock()/acquire() = 真正的锁(有排队等待)
@Slf4j
public class AbstractQueuedSynchronizerDemo extends AbstractQueuedSynchronizer {
public static void main(String[] args) {
AbstractQueuedSynchronizerDemo sync = new AbstractQueuedSynchronizerDemo();
// 直接CAS ≠ 锁(缺少排队):
// 成功了就直接执行,失败就放弃。直接操作状态。
// ExecutorService executorService = Executors.newFixedThreadPool(10);
// for (int i = 0; i < 10; i++) {
// executorService.execute(() -> {
// if (sync.compareAndSetState(0, 1)) {
// try {
// log.error("cas成功 {}", sync.getState());
// // 执行临界区代码...
// } finally {
// // 关键!执行完后要释放锁
// sync.setState(0); // 重置状态
// log.error("cas重置状态 {}", sync.getState());
// }
// } else {
// log.error("cas失败 {}", sync.getState());
// }
// });
// }
// 只调用tryAcquire ≠ 锁(还是缺少排队):
// 成功了就直接执行,失败就放弃。虽然用了AQS方法,但没有使用acquire()的排队功能。
// ExecutorService executorService = Executors.newFixedThreadPool(10);
// for (int i = 0; i < 10; i++) {
// executorService.execute(() -> {
// if (sync.tryAcquire(1)) {
// try {
// log.error("cas成功 {}", sync.getState());
// // 执行临界区代码...
// } finally {
// // 关键!执行完后要释放锁
// sync.release(1); // 重置状态
// log.error("cas重置状态 {}", sync.getState());
// }
// } else {
// log.error("cas失败 {}", sync.getState());
// }
// });
// }
// 真正的锁(有排队等待)
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
sync.lock();
try {
log.error("cas成功 {}", sync.getState());
// 执行临界区代码...
} finally {
// 关键!执行完后要释放锁
sync.unlock(); // 重置状态
log.error("cas重置状态 {}", sync.getState());
}
});
}
}
@Override
protected boolean tryAcquire(int arg) {
// 可以使用compareAndSetState,因为从父类继承
return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease(int arg) {
setState(0);
return true;
}
public void lock() {
acquire(1);
}
public void unlock() {
release(1);
}
}
浙公网安备 33010602011771号