Java并发之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))

  1. 线程 A 调用 lock()
  2. lock() 内部会调用 AQS 的 acquire(1)
  3. acquire(1) 的流程如下:
    1. tryAcquire(1):子类(ReentrantLock 的 Sync)实现此方法。检查 state
      • 如果 state == 0(锁空闲),则通过 CAS 将其设为 1,并设置当前线程为独占线程,返回 true。流程结束,线程 A 获得锁。
      • 如果 state != 0,但独占线程就是线程 A 自己(锁重入),则将 state 加 1,返回 true
      • 否则,返回 false
    2. 如果 tryAcquire 返回 false(获取失败),则调用 addWaiter(Node.EXCLUSIVE)。将线程 A 包装成一个独占模式的 Node 节点,并采用 CAS 方式安全地插入到等待队列的尾部。
    3. 接着调用 acquireQueued(...)。这个方法是核心中的核心:
      • 它会让节点自旋地尝试获取锁(在它即将成为队首时)。
      • 如果还是失败,则会判断是否应该阻塞自己(通过前驱节点的 waitStatus)。
      • 最终,通过 LockSupport.park() 将线程 A 挂起(阻塞)

3.2 释放锁 (unlock() -> release(1))

  1. 线程 A 调用 unlock()
  2. unlock() 内部会调用 AQS 的 release(1)
  3. release(1) 的流程如下:
    1. tryRelease(1):子类实现此方法。将 state 减 1。如果 state 减到 0,表示锁完全释放,清空独占线程,返回 true
    2. 如果 tryRelease 返回 true,则它会找到等待队列中的头节点(head)。
    3. 如果头节点不为空且其状态有效,则调用 unparkSuccessor(Node node)
    4. 这个方法会使用 LockSupport.unpark(thread) 唤醒 头节点后继节点中第一个未被取消的线程(假设是线程 B)。
  4. 线程 B 被唤醒后,会从之前在 acquireQueued 中被 park() 的地方继续执行。
  5. 线程 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
  • 主流的 LockCondition 实现都是基于 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 是构建锁和同步器的框架,不是直接给业务开发者使用的类。
  • 核心机制:通过一个 volatilestate 表示状态,一个 FIFO 队列管理等待线程,一套 CAS 操作保证状态更新的原子性。
  • 设计模式:模板方法模式。使用者继承 AQS 并重写指定方法,将 AQS 组合在自定义同步组件的实现中。
  • 两种模式独占模式(一次只有一个线程能执行,如 ReentrantLock)和共享模式(多个线程可同时执行,如 Semaphore/CountDownLatch)。
  • 重要性:理解了 AQS,就理解了 JUC 包中大部分同步工具的实现原理,是 Java 并发编程进阶的必经之路。

通过 AQS,Java 提供了一种高效、安全且可扩展的方式来构建复杂的同步结构,极大地简化了并发编程的难度。

AbstractQueuedSynchronizer的方法

AQS要解决的问题线程间同步协作的通用逻辑(排队、阻塞、唤醒)。“如何让多个线程按照既定规则有序地访问共享资源?”

AQS 的核心思想是:

  • 一个 volatile 的 int 类型状态变量(state):表示共享资源的状态。
  • 一个内置的 FIFO 双向队列(CLH队列的变体):用于管理所有获取资源失败的线程。

AQS 将方法分为两类:

  1. 面向使用者的模板方法:这些是 public final 方法,使用者直接调用,如 acquire, release。它们定义了同步器的基本工作流程。
  2. 需要子类重写的钩子方法:这些是 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)为例,看一下 acquirerelease 的流程:

acquire(int arg) 流程:
  1. 调用子类重写的 tryAcquire(arg) 尝试直接获取资源。
  2. 如果成功,方法结束。
  3. 如果失败,AQS 会将当前线程包装成一个节点(Node)并加入等待队列尾部
  4. 然后,在队列中自旋,检查自己的前驱节点是否是头节点(即下一个该轮到自己了)。
  5. 如果是头节点,则再次调用 tryAcquire(arg) 尝试获取。
  6. 如果成功,将自己设为新的头节点,并退出。
  7. 如果失败或还不是头节点,则可能挂起线程,等待被前驱节点唤醒。
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) 流程:
  1. 调用子类重写的 tryRelease(arg) 尝试释放资源。
  2. 如果释放成功(例如,state 变为 0),则检查队列中是否有等待的线程。
  3. 如果有,则唤醒头节点的后继节点
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):

  1. 调用 tryAcquire 尝试获取。
  2. 失败后,将线程包装成 EXCLUSIVE 节点加入队尾。
  3. 当前驱节点是头节点时,再次尝试 tryAcquire
  4. 成功后,将自己设为新头节点,流程结束。

共享模式 (acquireShared):

  1. 调用 tryAcquireShared 尝试获取。
  2. 如果返回值 >= 0(获取成功且有剩余资源),流程结束。
  3. 如果返回值 < 0(获取失败),将线程包装成 SHARED 节点加入队尾。
  4. 当前驱节点是头节点时,再次尝试 tryAcquireShared
  5. 如果成功且返回值 >= 0,不仅将自己设为新头节点,还会调用 doReleaseShared() 尝试唤醒后续的共享节点(这就是“传播”)。
2. 释放资源 (release vs releaseShared)

独占模式 (release):

  1. 调用 tryRelease,如果返回 true(表示资源完全释放)。
  2. 检查队列,唤醒头节点的下一个节点(如果存在)。

共享模式 (releaseShared):

  1. 调用 tryReleaseShared,如果返回 true(表示资源释放成功)。
  2. 调用 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)

这个例子完美展示了:

  • 独占模式:用于实现简单的互斥锁
  • 共享模式:用于实现更复杂的同步语义(如读写锁),提供更好的并发性能
问题

为什么tryAcquireSharedtryReleaseShared 实现中要 for (;;) {} 循环?包括jdk自己实现的StampedLockCountDownLatchReentrantReadWriteLock都有。

解答:

tryAcquireSharedtryReleaseShared 实现中的 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 (;;) {} 循环在共享模式中的必要性:

  1. 解决竞态条件:在读取状态和CAS更新之间,状态可能被其他线程修改
  2. 实现无锁算法:通过重试而不是阻塞来实现线程安全
  3. 保证正确性:确保在并发环境下资源计数的准确性
  4. 提高性能:避免使用重量级锁,在高度竞争时通过自旋重试

这种模式是乐观锁的典型实现:先读取,计算新值,然后尝试更新,如果失败就重试。这在读多写少的高并发场景中性能很好。

相比之下,独占模式通常更简单,因为状态变化是二元的(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);
    }
}

posted @ 2025-10-17 14:29  deyang  阅读(20)  评论(0)    收藏  举报