【Java】Sync

CAS

CAS(Compare And Swap,比较并交换)是多线程编程中的一种无锁并发控制技术,通过硬件指令实现原子操作,避免传统锁机制的性能开销。其核心思想是通过比较内存值与预期值来决定是否更新,整个过程由CPU保证原子性。以下是详细解析:


⚙️ CAS的核心原理

  1. 三步操作(原子性) CAS操作包含三个关键参数:
    • 内存地址(V):要修改的共享变量内存位置。
    • 预期原值(A):线程认为变量当前应有的值。
    • 新值(B):若内存值等于预期值,则更新为此值。 伪代码表示:
    boolean CAS(V, A, B) {
        if (V == A) {   // 比较内存值与预期值
            V = B;      // 相等则更新为新值
            return true;
        }
        return false;    // 否则操作失败
    }
    
    原子性保障:这三步操作由单条CPU指令(如x86的lock cmpxchg)完成,执行期间不会被中断。
  2. 工作流程示例 假设共享变量 count=0,两个线程并发执行count++
    • 线程1读取count=0,执行CAS(0, 1):内存值0等于预期值0,更新为1,成功。
    • 线程2读取count=0(未及时刷新),执行CAS(0, 1):内存值1≠预期值0,操作失败 → 重试读取count=1,再执行CAS(1, 2),成功。

⚖️ CAS的优缺点

优点

  1. 无锁高性能
    • 避免线程阻塞和上下文切换,适合低竞争场景(如计数器增减)。
    • 吞吐量高于锁机制,尤其在细粒度操作中(如AtomicInteger自增)。
  2. 乐观并发
    • 假设操作冲突少,失败时通过自旋重试(循环CAS)而非阻塞。

⚠️ 缺点

  1. ABA问题
    • 若变量值从 A → B → A,CAS无法感知中间变化,可能误判“未被修改”。
    • 解决方案:使用带版本号的原子类(如AtomicStampedReference),每次更新递增版本号。
  2. 自旋开销
    • 高竞争场景下频繁失败重试,导致CPU空转(如100个线程争抢同一变量)。
  3. 单一变量限制
    • 只能保证一个共享变量的原子操作。多变量需合并或改用锁。

⚡️ Java中的CAS实现

Java通过 java.util.concurrent.atomic 包提供原子类,底层调用Unsafe类的CAS指令:

// 使用AtomicInteger实现线程安全自增
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子自增,无需锁
  • 关键方法:
    • compareAndSet(expect, update):核心CAS操作。
    • getAndIncrement():循环CAS实现自增。
  • 底层机制: 依赖sun.misc.Unsafe#compareAndSwapInt,通过JNI调用CPU指令。

🔧 典型应用场景

  1. 无锁计数器AtomicInteger,替代synchronized实现高效计数。
  2. 自旋锁(SpinLock)通过CAS实现轻量级锁:
    public class SpinLock {
        private AtomicReference<Thread> owner = new AtomicReference<>();
        public void lock() {
            while (!owner.compareAndSet(null, Thread.currentThread())) {
                // 自旋等待
            }
        }
        public void unlock() {
            owner.compareAndSet(Thread.currentThread(), null);
        }
    }
    
  3. 并发容器 ConcurrentHashMap的桶节点操作、LinkedTransferQueue等使用CAS减少锁竞争。

↔️ CAS vs 锁机制

特性CAS锁(如synchronized)
机制乐观锁(无锁)悲观锁(阻塞)
原子性粒度单一变量代码块/方法
性能低竞争时高效,高竞争时CPU空转高竞争时稳定,但上下文切换开销大
适用场景简单原子操作(自增、标志位更新)复合操作(如转账:先扣A再增B)
问题ABA问题、自旋开销死锁、优先级反转

选择原则

  • 简单变量且低竞争 → CAS(如计数器)。
  • 复杂逻辑或高竞争 → (如synchronizedReentrantLock)。

💎 总结

  • CAS本质:硬件级原子指令,通过“比较-交换”实现无锁并发。
  • 核心价值:高性能线程安全操作,避免锁开销。
  • 使用注意:
    • 优先用于单一共享变量的简单操作。
    • 高竞争场景需监控CPU占用,必要时切换为锁。
    • ABA问题需通过版本号解决(AtomicStampedReference)。 合理运用CAS可在特定场景显著提升并发性能,但需严格评估其适用性,避免盲目替代锁机制。

AtomicStampedReference

AtomicStampedReference 是 Java 并发包(java.util.concurrent.atomic)中解决 CAS 操作 ABA 问题的核心工具。它通过为对象引用附加一个整数版本戳(Stamp),实现对引用变化的版本追踪,确保并发修改的安全性。以下从原理、实现到应用场景展开分析:


🔍 ABA 问题与设计动机

  1. ABA 问题本质
    • 场景描述: 线程 T1 读取共享变量值为 A → T2 将值改为 B 后再改回 A → T1 执行 CAS 操作,发现值仍为 A 并成功更新。此时 T1 ​无法感知中间状态变化​(A→B→A)。
    • 危害:在无锁数据结构(如栈、队列)中可能导致数据丢失或逻辑错误。
      • 示例:无锁栈的 pop() 操作中,若节点被移除后重新入栈,CAS 可能误判为未变化,导致栈结构破坏。
    • 金融场景:账户余额被反复修改后还原,导致重复扣款(如 100→50→100→扣款 50 成功)。
  2. AtomicStampedReference 的解决方案
    • 核心机制: 将对象引用 V 与整数戳 int stamp 绑定,每次更新时戳递增(类似版本号)。CAS 操作需同时校验引用值和戳,任一不匹配则失败。
    • 类比:检查物品时不仅看是否存在,还需确认版本号是否变化(如“房间物品未少,但版本号已变,说明曾被移动”)。

⚙️ 实现原理与源码解析

  1. 内部数据结构
    • 不可变 Pair 类:封装引用 reference 和戳 stamp,保证原子替换时的状态一致性。
       private static class Pair<T> {
          final T reference;
       final int stamp; // 版本戳
      }
      
  • AtomicReference 封装volatile AtomicReference<Pair<V>> pair 确保内存可见性,底层通过 UnsafeVarHandle 实现 CAS。
  1. 核心方法 compareAndSet
    • 原子性保证:同时校验预期引用和戳,通过则更新为新引用和新戳:
      public boolean compareAndSet(V expectedReference, V newReference,
                                 int expectedStamp, int newStamp) {
          Pair<V> current = pair.get();
          return expectedReference == current.reference &&  // 校验引用
                 expectedStamp == current.stamp &&          // 校验戳
                 ((newReference == current.reference &&     // 避免无意义更新
                   newStamp == current.stamp) ||
                  casPair(current, Pair.of(newReference, newStamp))); // 执行 CAS
      }
      
    • 短路优化:若新值与当前值相同,直接返回 true 避免 CAS 开销。
  2. 戳的管理
    • 递增规则:戳通常单调递增(如 stamp+1),防止状态回滚。
    • 获取方法:
      • get(int[] stampHolder):返回当前引用,并将戳存入数组 stampHolder[0]
      • getStamp():直接返回当前戳。

⚡️ 典型应用场景

  1. 无锁数据结构
    • 栈/队列节点更新:每次 push/pop 操作时递增戳,防止节点被复用导致的 ABA 问题。
      // 无锁栈示例(简化)
      public void push(T value) {
          Node<T> newNode = new Node<>(value);
          while (true) {
              Node<T> oldTop = top.getReference();
              int stamp = top.getStamp();
              newNode.next = oldTop;
              if (top.compareAndSet(oldTop, newNode, stamp, stamp + 1)) 
                  break;
          }
      }
      
  2. 状态机与资源管理
    • 订单状态流转: 状态从 UNPAID→PAID→REFUND→UNPAID 时,戳的变化可避免误判为初始状态。
    • 分布式开关控制:服务启停时通过戳校验防止重复操作:
      public class SafeSwitch {
          private AtomicStampedReference<Boolean> status = 
              new AtomicStampedReference<>(false, 0);
      
          public boolean turnOn() {
              int[] stamp = new int[1];
              Boolean current = status.get(stamp);
              return !current && 
                     status.compareAndSet(false, true, stamp[0], stamp[0] + 1);
          }
      }
      
  3. 资源池对象复用
    • 对象从池中取出→修改→放回时,戳可检测对象是否被篡改。

⚖️ 与其他方案的对比

方案原理ABA 防护适用场景性能开销
AtomicStampedReference引用 + 整数版本戳强(状态空间大)需精细版本控制的场景
AtomicMarkableReference引用 + 布尔标记弱(仅两种状态)简单状态标记(如“已读”)
乐观锁(数据库)版本号或时间戳数据库事务
互斥锁完全串行化无风险复杂事务

💡 选型建议

  • 需严格版本控制时选 AtomicStampedReference
  • 仅需二元状态时用 AtomicMarkableReference 更轻量。

⚠️ 局限性与注意事项

  1. 戳溢出风险
    • int 类型戳可能从 Integer.MAX_VALUE 溢出到 Integer.MIN_VALUE,导致版本回退误判。
    • 解决方案:监控戳范围或使用 long 类型(需自定义实现)。
  2. 性能开销
    • 每次 CAS 需创建新 Pair 对象,增加 GC 压力。
    • 在无竞争场景下,性能优于锁;高竞争时可能因自旋消耗 CPU。
  3. 正确使用规范
    • 戳必须递增:若回滚戳值(如重置为旧值),ABA 问题可能重现。
    • 强制校验戳:业务逻辑需依赖戳的变化,而非仅引用值。

💎 总结

AtomicStampedReference 通过引用+版本戳的二元校验机制,为无锁并发提供了可靠的 ABA 问题解决方案。其核心价值在于:

  1. 状态追踪:戳的递增唯一标识引用变化历史,避免隐蔽的中间状态;
  2. 无锁并发:基于 CAS 实现高性能线程安全,适用于栈、队列、状态机等场景;
  3. 责任转移:将 ABA 风险从业务逻辑剥离至底层框架,提升代码健壮性。

实际开发中,优先用于需严格版本控制的并发结构,并遵循 戳递增+及时清理 原则,以规避溢出与性能陷阱。

volatile

volatile 是 Java 中用于多线程编程的关键字,主要解决共享变量的可见性指令重排序问题,但不保证原子性。以下是详细解析:


⚙️ 核心作用

  1. 内存可见性(Visibility)
    • 普通变量的修改可能仅存在于线程的本地内存(如 CPU 缓存),其他线程无法立即感知。
    • volatile 变量每次写操作直接刷新到主内存每次读操作从主内存重新加载,确保所有线程看到最新值。
    • 示例:
      private volatile boolean running = true;
      // 线程A修改 running=false 后,线程B能立即退出循环
      while (running) { /* 任务 */ }
      
  2. 禁止指令重排序(Ordering)
    • 编译器和处理器可能为优化性能重排指令顺序,导致多线程逻辑错误。
    • volatile 通过内存屏障(Memory Barrier)禁止重排序:
      • 写操作前插入 StoreStore 屏障,后插入 StoreLoad 屏障。
      • 读操作前插入 LoadLoad 屏障,后插入 LoadStore 屏障。
    • 典型场景:单例模式的双重检查锁定(Double-Checked Locking):
      public class Singleton {
          private static volatile Singleton instance;
          public static Singleton getInstance() {
              if (instance == null) {
                  synchronized (Singleton.class) {
                      if (instance == null) {
                          instance = new Singleton(); // 避免重排序导致未初始化完成的对象暴露
                      }
                  }
              }
              return instance;
          }
      }
      

⚠️ 局限性:不保证原子性

  • 原子性问题volatile 仅保证单次读/写的原子性(如 long/double 的 64 位操作),但复合操作(如 i++)仍非原子,因为包含“读-改-写”三步。
  • 解决方案
    • 使用 synchronizedLock 保证代码块原子性。
    • 使用 AtomicInteger 等原子类(基于 CAS 实现)。

🔧 实现原理

  1. 内存屏障 JVM 在编译时插入屏障指令,强制刷新主内存数据并限制重排序。
  2. 缓存一致性协议(如 MESI) 多核 CPU 通过嗅探总线,发现 volatile 变量修改后,将其他核心的缓存行设为无效,强制从主内存加载新值。

🛠️ 适用场景

  1. 状态标志位 简单布尔变量控制线程执行(如停止循环)。
  2. 一次性发布 确保对象初始化完成前不被引用(如单例模式)。
  3. 独立观察 定期更新供多线程读取的变量(如心跳标记)。

↔️ volatile vs synchronized

特性volatilesynchronized
可见性✅ 直接读写主内存✅ 锁释放时刷新主内存
有序性✅ 禁止重排序✅ 串行执行
原子性❌ 仅单次读/写✅ 代码块/方法级
阻塞❌ 无阻塞✅ 线程竞争锁时阻塞
性能开销低(无上下文切换)高(锁竞争、内核切换)

选择原则

  • 仅需可见性/有序性 → volatile(如状态标志)。
  • 需原子性或复合操作 → synchronized 或原子类。

⚠️ 注意事项

  1. 非万能替代锁:仅当变量独立于其他状态时适用(如 flag 不依赖其他变量值)。
  2. 性能影响:频繁读写 volatile 变量因主内存同步略慢于普通变量,但仍优于锁。 通过合理使用 volatile,可在轻量级同步需求中平衡性能与线程安全,但需严格区分其与锁的适用边界。

synchronized

以下是对 Java 中 synchronized 关键字的详细解析,结合其作用、用法、底层实现及最佳实践:


⚙️ synchronized 的核心作用

  1. 互斥性(Mutex)
    • 确保同一时刻仅有一个线程能执行被修饰的代码块或方法,避免多线程并发修改共享资源导致的竞态条件(Race Condition)。
  2. 可见性(Visibility)
    • 线程在释放锁前,强制将本地内存中的变量修改刷新到主内存;获取锁时,从主内存重新加载变量值,保证其他线程看到最新数据。
  3. 有序性(Ordering)
    • 通过锁机制禁止编译器或处理器对同步代码块内的指令重排序,确保代码按顺序执行。

🛠️ synchronized 的三种用法

修饰实例方法

  • 锁对象:当前实例(this)。
  • 作用范围:整个方法体。
public synchronized void increment() {
    count++;  // 线程安全的复合操作
}
  • 适用场景:保护对象实例内的共享变量。

修饰静态方法

  • 锁对象:类的 Class 对象(如 MyClass.class)。
  • 作用范围:所有实例共享同一把锁,影响整个类的静态资源
public static synchronized void staticMethod() {
    // 操作静态变量
}
  • 适用场景:保护静态变量或类级资源。

修饰代码块

  • 锁对象:任意显式指定的对象(如 Object lock = new Object())。
  • 作用范围:仅同步代码块内部,支持细粒度控制。
public void method() {
    synchronized (lockObj) {  // 自定义锁对象
        // 仅同步关键代码
    }
}
  • 适用场景:减少锁范围,提升并发性能。

🔍 三种用法对比

用法类型锁对象作用域适用场景
修饰实例方法this整个方法体保护实例变量
修饰静态方法Class 对象整个类保护静态变量
修饰代码块自定义对象代码块内部细粒度控制,减少锁竞争

⚙️ 底层实现原理

  1. Monitor 机制
    • 每个 Java 对象关联一个监视器锁(Monitor),通过字节码指令 monitorentermonitorexit 实现锁的获取与释放。
    • 线程进入同步块时尝试获取锁,成功则执行代码;失败则阻塞直至锁释放。
  2. 锁升级优化(JDK 1.6+)
    • 无锁 → 偏向锁:首次获取锁时标记偏向线程,避免重复 CAS 开销。
    • 偏向锁 → 轻量级锁:多线程竞争时,通过 CAS 自旋尝试获取锁(减少阻塞)。
    • 轻量级锁 → 重量级锁:自旋失败后升级为操作系统级互斥锁(Mutex),线程进入阻塞状态。
    • 目的:平衡性能与线程安全,减少锁操作的开销。

⚠️ 使用注意事项与最佳实践

  1. 避免死锁
    • 确保多线程获取锁的顺序一致(如:按固定顺序锁定多个对象)。
    // 示例:按锁对象的固定顺序加锁
    if (obj1.lockOrder() > obj2.lockOrder()) {
        synchronized(obj2) {
            synchronized(obj1) { ... }
        }
    }
    
  2. 减小锁粒度
    • 优先使用同步代码块替代同步方法,仅锁定必要资源(如 FineGrainLock 类中的分段锁)。
  3. 替代方案
    • 高并发场景下,考虑 ReentrantLock(支持公平锁、可中断)或 StampedLock(乐观读锁)提升性能。
  4. 单例模式的双重检查锁定(DCL)
    • 结合 volatile 防止指令重排序导致未初始化完成的对象被暴露:
    public class Singleton {
        private static volatile Singleton instance;
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

↔️ synchronized vs volatile

特性synchronizedvolatile
原子性✅ 保证复合操作(如 i++❌ 仅保证单次读/写的原子性
可见性✅ 通过锁刷新主内存✅ 直接读写主内存
有序性✅ 禁止同步块内指令重排序✅ 禁止变量相关指令重排序
互斥性✅ 同一时间仅一个线程访问❌ 允许多线程并发读写
性能开销较高(锁竞争、上下文切换)较低(无锁机制)
适用场景复杂逻辑或多步操作单一状态标志(如 boolean flag

选择原则

  • 需原子性 → synchronized 或原子类(AtomicInteger)。
  • 仅需可见性 → volatile(如状态标志位)。

💎 总结

synchronized 是 Java 线程安全的基石,通过 互斥性、可见性、有序性 解决并发问题。使用时需注意:

  1. 锁粒度:优先用代码块缩小同步范围。
  2. 锁顺序:避免死锁,固定多锁获取顺序。
  3. 性能权衡:高并发场景考虑 ReentrantLock 等替代方案。
  4. 复合操作:确保原子性需依赖锁机制而非 volatile。 合理运用 synchronized 可高效构建线程安全程序,但需结合场景选择最优同步策略。

Object Monitor

Object Monitor(监视器锁)是 Java 中实现线程同步的核心底层机制,与每个 Java 对象关联,通过 synchronized关键字触发。其工作原理基于 JVM 底层的 ObjectMonitor 类(C++实现),以下是详细解析:


🔒 Monitor 锁的核心结构

每个 Java 对象在堆内存中均关联一个 ObjectMonitor 对象,关键字段如下:

字段作用
_owner指向持有锁的线程(null 表示锁未被占用)
_EntryList存储等待获取锁的阻塞线程队列(竞争锁失败时进入)
_WaitSet存储因调用 wait() 而主动放弃锁的线程队列(等待唤醒)
_recursions记录锁的重入次数(同一线程多次获取锁时递增)
_count锁计数器(线程获取锁时 +1,释放时 -10 表示可被竞争)

📌 对象头关联:对象的 Mark Word 中锁标志位为 10 时(重量级锁),存储指向 ObjectMonitor 的指针。


⚙️ Monitor 锁的工作流程

加锁流程(monitorenter 指令)

  • 无竞争:若 _ownernull,线程通过 CAS 操作将 _owner 设为自己,_count_recursions 置为 1
  • 锁重入:若 _owner 是当前线程,_recursions++(体现可重入性)。
  • 竞争失败:
    • 线程进入 _EntryList 队列阻塞(Linux 下通过 pthread_mutex_lock 实现 OS 级阻塞)。
  • 若开启自旋锁(JDK 6+ 默认),线程先自旋尝试获取锁(避免立刻阻塞)。

释放锁(monitorexit 指令)

  • 将 _recursions 减 1 ,若结果为 0 :
    • 重置 _ownernull_count1
  • 唤醒 _EntryList 中的首个线程(非公平锁)或按 FIFO 唤醒(公平锁)。

wait()notify() 机制

  • wait():
    • 持有锁的线程调用 wait()后:
      1. 释放锁(_owner=null_count--)。
      2. 线程封装为 ObjectWaiter 节点加入 _WaitSet 队列。
      3. 线程进入阻塞状态,等待唤醒。
  • notify():
    • _WaitSet 中移出首个线程节点,加入 _EntryList 参与锁竞争。
    • notifyAll():移出 _WaitSet 中所有线程节点。

⚠️ 注意:调用 wait()/notify() 前需持有锁,否则抛出 IllegalMonitorStateException


🔄 锁升级与重量级锁的关系

当轻量级锁/偏向锁竞争加剧时,会升级为重量级锁(Monitor 锁):

  1. 升级条件:
    • 自旋获取轻量级锁失败(默认自旋 10 次)。
    • 多线程激烈竞争同一资源。
  2. 升级过程:
    • 对象头中的锁标志位变为 10(重量级锁)。
    • Mark Word 指向 ObjectMonitor 地址。
  3. 性能影响:
    • 优点:彻底解决竞争问题(通过 OS 阻塞队列)。
    • 缺点:线程阻塞/唤醒涉及用户态-内核态切换,性能开销大(微秒级)。

⚖️ Monitor 锁的特性总结

特性说明
可重入性同一线程多次获取锁时,_recursions 递增。
非公平性默认唤醒 _EntryList 队首线程(允许新线程插队竞争,提高吞吐)。
条件等待通过 _WaitSet 实现精细的线程等待/唤醒机制。
互斥性仅一个线程可持有锁(_owner 唯一性)。

💡 应用场景与注意事项

  1. 适用场景:
    • 高竞争、长耗时同步操作(如数据库连接池)。
    • 需精细控制线程等待/唤醒的复杂同步逻辑(如生产者-消费者模型)。
  2. 优化建议:
    • 减少同步范围(如用同步块替代同步方法)。
    • 高并发场景可改用 ReentrantLock(支持超时、可中断)。

💎 核心价值:Monitor 锁通过 OS 级阻塞队列精准线程调度,为 Java 高竞争场景提供了终极线程安全解决方案,但需权衡其性能开销。

synchronized 锁升级

以下是Java中偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)和重量级锁(Heavyweight Locking)的详细解析及其核心区别,结合底层实现原理与适用场景展开说明:


🔒 锁的升级机制与设计背景

Java的synchronized锁在JDK 1.6后引入锁升级策略(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),目的是在无竞争时降低开销高竞争时保证线程安全

  • 升级触发条件:
    • 偏向锁 → 轻量级锁:首次出现线程竞争时。
    • 轻量级锁 → 重量级锁:自旋失败(默认10次)或竞争加剧。
  • 不可降级:锁升级后无法回退,避免状态转换开销。

⚙️ 三种锁的核心原理与特点

偏向锁(Biased Locking)

  • 核心思想:优化单线程重复加锁场景。首次获取锁时记录线程ID到对象头(Mark Word),后续同一线程直接进入同步块,无需同步操作。
  • 实现机制:
    • 对象头标记位:biased_lock=1, lock=01
    • 通过CAS记录线程ID,成功后锁进入偏向模式。
  • 适用场景:单线程或线程交替执行同步块(如局部代码块)。
  • 缺点:竞争时需撤销偏向锁(STW暂停持有线程),开销较大。
  • 现状:JDK 15后默认禁用(高竞争场景下撤销成本过高)。

轻量级锁(Lightweight Locking)

  • 核心思想:通过CAS自旋避免线程阻塞,减少用户态/内核态切换开销。
  • 实现机制:
    • 线程栈中创建锁记录(Lock Record),复制对象头的Mark Word(Displaced Mark Word)。
  • 通过CAS将对象头指向锁记录地址,成功则获取锁(标记位lock=00)。
    • 失败时自旋重试(自适应自旋:根据历史成功率动态调整自旋次数)。
  • 适用场景短时操作低竞争(如计数器自增)。
  • 缺点:自旋消耗CPU,高竞争时效率低。

重量级锁(Heavyweight Locking)

  • 核心思想:依赖操作系统互斥量(Mutex) 实现线程阻塞,彻底解决竞争问题。
  • 实现机制:
    • 对象头指向操作系统级Monitor(锁标志位lock=10)。
    • 竞争失败线程进入阻塞队列,由OS调度唤醒。
  • 适用场景高竞争长时同步操作(如数据库连接池)。
  • 缺点:线程阻塞/唤醒涉及上下文切换,性能损耗大(微秒级延迟)。

🔍 三种锁的核心区别对比

特性偏向锁轻量级锁重量级锁
竞争场景单线程重复访问多线程交替执行(无实际竞争)多线程激烈竞争
实现机制记录线程ID(无CAS)CAS自旋 + 栈帧锁记录操作系统Mutex + 阻塞队列
线程阻塞❌ 无阻塞❌ 自旋(不阻塞)✅ 阻塞(OS调度)
性能开销无竞争时最低低竞争时较高(CPU自旋)高竞争时最高(上下文切换)
适用场景单线程场景短时操作(<纳秒级)长时高竞争操作
锁撤销/升级竞争时升级为轻量级锁自旋失败升级为重量级锁无降级机制

💡 性能关键点

  • 偏向锁:无竞争时效率最高,但竞争时撤销成本高。
  • 轻量级锁:平衡点在于自旋时间 < 线程切换时间
  • 重量级锁:完全消除CPU空转,但切换开销固定。

⚠️ 锁升级流程与实战影响

  1. 升级路径
    graph LR  
    A[无锁] -->|首次加锁| B[偏向锁]  
    B -->|竞争发生| C[轻量级锁]  
    C -->|自旋失败/多线程竞争| D[重量级锁]  
    
    • 竞争触发升级后,对象头Mark Word结构永久变化。
  2. 工程实践建议
    • 避免过度同步:锁范围尽量缩小(如同步块代替同步方法)。
    • 监控锁竞争:使用jstackArthas查看锁状态(偏向/轻量/重量)。
    • 高并发场景:
      • 替代方案:ReentrantLock(可中断/超时)或StampedLock(乐观读)。
  • 禁用偏向锁:JDK 15+默认关闭,可通过-XX:-UseBiasedLocking显式禁用。

💎 总结

  • 偏向锁:为单线程设计,无竞争时效率极致,但竞争撤销成本高(JDK 15+已废弃)。
  • 轻量级锁:通过CAS自旋避免阻塞,适合短时低竞争操作,自旋失败则升级。
  • 重量级锁:终极方案,依赖OS互斥量,保证高竞争下的安全性,但性能代价最高。

选择本质:JVM根据竞争强度动态选择锁类型,开发者需关注锁状态与竞争情况,避免重量级锁成为瓶颈。高并发场景下,结合jstack分析锁竞争,必要时采用显式锁(如ReentrantLock)替代synchronized

偏向锁

偏向锁(Biased Locking)是Java虚拟机(JVM)为优化synchronized关键字性能而设计的锁机制,旨在减少单线程重复获取锁的开销。其核心思想是“偏向”首个获取锁的线程,后续无需同步操作。以下从原理、工作机制、优缺点到实践场景展开详解:


⚙️ 核心原理与设计目标

  1. 设计背景
    • 问题:传统锁(如重量级锁)依赖操作系统互斥量(Mutex),涉及线程阻塞/唤醒,导致上下文切换开销大6,8
    • 优化目标:针对单线程或低竞争场景,避免无意义的同步操作(如CAS),提升性能2,7
  2. 技术基础:对象头与Mark Word
    • 每个Java对象头部包含Mark Word字段(64位),存储锁状态、哈希码、GC分代年龄等信息2,4
    • 偏向锁状态下的Mark Word结构:
      | 锁标志位 (01) | 偏向线程ID (54 bits) | Epoch (2 bits) | 未使用 (1 bit) | 
      
      其中:
      • 偏向线程ID:记录首次获取锁的线程ID。
      • Epoch:用于批量重偏向的版本号,避免频繁撤销4,8

🔄 工作机制详解

偏向锁的获取流程

  • 步骤1:初始无锁状态 对象创建时,Mark Word为无锁状态(锁标志位012,7
  • 步骤2:首次获取锁 线程T1首次进入同步块:
    • JVM通过CAS操作将Mark Word的锁标志位改为偏向锁(01)。
    • 将T1的线程ID写入Mark Word4,8
    • 此时锁进入“偏向模式”。
  • 步骤3:再次获取锁 T1后续进入同步块时:
    • JVM检查Mark Word中的线程ID是否与T1匹配。
    • 若匹配:直接执行同步代码,无任何同步操作(如CAS或阻塞)2,9

偏向锁的撤销与升级

  • 触发条件:当线程T2尝试获取已被T1偏向的锁时4,8
  • 撤销过程:
    1. 暂停持有锁的线程T1(STW,Stop-The-World)。
    2. 检查T1是否仍活跃:若已退出同步块,则撤销偏向锁,恢复为无锁状态;若仍在执行,则升级为轻量级锁(锁标志位00)。
    3. 唤醒T1和T2,T2通过自旋(CAS) 竞争轻量级锁6,8
  • 批量重偏向优化: 若同一类的大量对象被不同线程交替访问,JVM会通过Epoch机制批量重偏向,避免频繁撤销4,8

锁升级路径

graph LR
  A[无锁状态] -->|单线程重复访问| B[偏向锁]
  B -->|多线程竞争| C[轻量级锁]
  C -->|竞争激烈| D[重量级锁]
  • 轻量级锁通过自旋CAS减少阻塞,重量级锁依赖操作系统互斥量3,6

⚖️ 优缺点分析

✅ **核心优势

  • 零同步开销:单线程重复访问时,无需CAS或线程调度,性能接近无锁7,9
  • 减少上下文切换:避免线程阻塞/唤醒,降低CPU资源消耗6,8

⚠️ **局限性

  • 撤销开销大:竞争触发撤销时需STW,暂停所有线程,延迟敏感场景性能下降4,8
  • 短生命周期对象无效:对象若被多个线程快速交替访问,偏向锁反复撤销反而降低效率4,9
  • 高竞争场景不适用:频繁竞争导致锁升级为重量级锁,性能反而不如直接禁用偏向锁6,8

🎯 适用场景与配置建议

理想场景

  • 单线程主导:如线程池中单线程处理任务、初始化阶段加载资源9
  • 低竞争环境:锁被同一线程长期持有,偶有其他线程尝试竞争(如后台监控线程)4,7
  • 示例:
    // 单线程重复访问同步块
    public void processTask() {
        synchronized(lock) {  // 首次获取后偏向当前线程
            // 业务逻辑
        }
    }
    

不适用场景

  • 高并发竞争(如秒杀系统)。
  • 锁对象生命周期短(如临时对象同步块)。
  • 需严格实时响应的系统(STW可能导致延迟波动)4,8

JVM参数配置

参数作用
-XX:+UseBiasedLocking启用偏向锁(JDK 6u23后默认开启)
-XX:-UseBiasedLocking禁用偏向锁(高竞争场景推荐)
-XX:BiasedLockingStartupDelay=0取消偏向锁延迟(默认延迟4秒,避免短生命周期对象无效优化)4,7

💡 注意:JDK 15后偏向锁默认关闭,JDK 18中彻底移除,因现代硬件环境下轻量级锁优化已足够高效4


🔄 与其他锁的对比

特性偏向锁轻量级锁重量级锁
适用场景单线程/低竞争多线程交替访问(低竞争)高并发竞争
实现机制记录线程ID,免同步操作CAS自旋尝试获取锁操作系统互斥量(Mutex)
性能开销接近零开销(无竞争时)低(自旋消耗CPU)高(上下文切换)
升级触发条件多线程竞争自旋失败或竞争激烈竞争超时或资源争用
适用锁类型synchronized方法/块synchronized方法/块synchronizedReentrantLock

来源:3,6,8


💎 总结

偏向锁通过消除单线程重复同步的开销,成为JVM锁优化的重要一环。其价值在于:

  • 性能提升:单线程场景同步效率接近无锁操作。
  • 资源节约:减少CPU上下文切换与CAS操作。
  • 平滑升级:与轻量级锁、重量级锁形成动态优化链路。 实践建议:在明确单线程主导的场景启用偏向锁;高并发或短生命周期对象场景建议禁用(-XX:-UseBiasedLocking)。现代JDK中,随着自旋优化与硬件发展,轻量级锁已能覆盖多数场景,偏向锁逐渐退出历史舞台,但其设计思想仍值得借鉴4,6,8

弃用偏向锁

偏向锁(Biased Locking)在JDK 15中被默认关闭(通过JEP 374实现),并在后续版本中被废弃甚至移除。这一决策是JVM团队基于现代硬件特性、应用场景变化和性能实测数据的综合考量,主要原因如下:


🔧 性能收益与成本失衡

  1. 优化场景减少
    • 偏向锁的初衷是优化单线程重复访问同步块的场景(如初始化阶段或线程局部操作),通过避免CAS操作降低开销2,3
    • 现代应用变化:微服务、高并发系统中,锁对象常被多线程竞争访问(如线程池处理请求),偏向锁的适用场景大幅减少2,5
  2. 撤销开销过高
    • 当其他线程尝试获取已被偏向的锁时,JVM需触发 撤销操作:
      • 暂停持有偏向锁的线程(Stop-The-World, STW),等待全局安全点(Safepoint)2,6
      • 升级为轻量级锁或重量级锁,涉及线程状态检查和Mark Word更新4,5
    • 高并发场景下:频繁撤销导致STW操作激增,成为延迟抖动和吞吐量下降的主因3,5
  3. 对比收益不足
    • 现代硬件(多核CPU)和JIT优化(如锁消除、自适应自旋)使轻量级锁的性能接近偏向锁,且无撤销成本3,5
    • 官方基准测试(如SPECjbb2015)显示:禁用偏向锁后,99%的延迟波动降低5%-10%,吞吐量无显著损失3

⚙️ 实现复杂性与维护负担

  1. 代码耦合度高
    • 偏向锁的实现(如biased_locking.cpp)与HotSpot的锁子系统深度耦合,约占JVM代码量的2%,增加了维护难度和潜在Bug风险1,3
    • 撤销逻辑涉及安全点机制、线程状态管理等复杂交互,阻碍了JVM其他特性的演进(如GraalVM即时编译优化)2,4
  2. 与其他机制冲突
    • HashCode调用:在偏向锁状态下调用hashCode()会强制撤销锁并升级,引发额外性能损耗6
    • 短期对象无效:大量短暂对象(如HTTP请求上下文)的锁竞争直接跳过偏向阶段,使其优化无效3,5

🔄 现代硬件与并发模型的演进

  1. 多核处理器普及
    • 服务器普遍具备数十核,线程竞争频率显著增加,偏向锁的“单线程假设”与高并发场景不匹配3,5
  2. 轻量级锁足够高效
    • 轻量级锁通过CAS自旋处理低竞争场景,避免了用户态-内核态切换,性能已接近偏向锁4,6
    • 适应性自旋优化:JVM动态调整自旋次数(基于历史竞争情况),减少无效CPU消耗5
  3. 无锁编程的兴起
    • java.util.concurrent包提供的原子类(如AtomicInteger)、并发容器(如ConcurrentHashMap)替代了部分同步块需求2,3

⚖️ 开发者影响与应对建议

  1. 默认行为变化
    • JDK 15+:默认关闭偏向锁(等价于
      -XX:-UseBiasedLocking
      
      ),锁升级路径简化为:
      无锁 → 轻量级锁 → 重量级锁
      
  2. 性能调优建议
    场景建议
    单线程高频访问同步块通过-XX:+UseBiasedLocking手动启用,但需测试兼容性(未来版本可能移除)2,3
    高并发应用接受默认配置,依赖轻量级锁优化
    延迟敏感型系统优先使用无锁数据结构(如LongAdder)或显式锁(如ReentrantLock3,5
  3. 代码优化方向
    • 减少同步块粒度:缩小临界区范围,避免长时间持有锁。
    • 锁消除与粗化:依赖JIT编译器自动消除无效锁,或合并连续锁操作5,6

💎 总结

偏向锁的废弃是JVM适应时代发展的必然结果:

  1. 性能层面:撤销成本(STW)远超单线程优化收益,轻量级锁成为更普适的低开销方案3,5
  2. 工程层面:简化JVM锁子系统,提升可维护性,为未来优化腾出空间1,4
  3. 生态层面:推动开发者转向更适应高并发的同步策略(如无锁编程、显式锁控制)2,6

最终建议:除非明确需兼容遗留单线程高频锁场景,否则无需主动启用偏向锁。JDK 15+的默认配置已针对现代硬件和主流应用优化,性能更稳定可靠。

Unsafe

Unsafe 类是 Java 中位于 sun.misc 包下的特殊工具类,提供直接操作内存、线程调度、对象实例化等底层能力,因其绕过 JVM 安全机制而得名“不安全”。它是 JDK 内部实现高性能框架的核心,但普通开发需谨慎使用。以下是详细解析:


⚙️ 核心功能与原理

内存操作

直接分配、读写堆外内存,避免 GC 开销:

  • allocateMemory(long bytes):分配指定字节的堆外内存。
  • putXXX(address, value) / getXXX(address):直接读写内存数据(如 putIntgetLong)。
  • freeMemory(address):手动释放内存,否则导致内存泄漏。 ​用途​:Netty 的 DirectByteBuffer、高频 I/O 场景。

对象操作

绕过访问控制,直接修改对象字段:

  • objectFieldOffset(Field f):获取字段内存偏移量。
  • putObject(Object o, long offset, Object value):通过偏移量修改字段(无视 private 修饰)。 ​示例​:
User user = new User();
long offset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
unsafe.putInt(user, offset, 25); // 强制修改age字段

CAS(原子操作)

通过 CPU 指令实现无锁并发:

  • compareAndSwapInt(Object o, long offset, int expected, int update):若当前值等于预期值,则更新。 ​用途​:AtomicIntegerConcurrentHashMap 的底层实现。

线程调度

精准控制线程阻塞与唤醒:

  • park(boolean isAbsolute, long timeout):挂起当前线程(类似 LockSupport.park)。
  • unpark(Thread thread):唤醒指定线程。 ​用途​:StampedLock、AQS 的阻塞队列实现。

类与对象实例化

绕过构造函数创建对象:

  • allocateInstance(Class<?> clazz):不调用构造方法直接实例化对象(对象字段为默认值)。 ​风险​:破坏对象不变性(如 final 字段未初始化)。

🔍 获取 Unsafe 实例的方法

由于设计限制,常规方法无法直接获取:

  1. 反射获取单例(最常用):
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    Unsafe unsafe = (Unsafe) field.get(null);
    
  2. 引导类加载器加载:通过 -Xbootclasspath/a 参数强制由 Bootstrap ClassLoader 加载调用类。

⚠️ 主要风险与争议

安全风险

  • 内存泄漏:需手动释放内存,否则持续占用资源。
  • JVM 崩溃:非法内存访问(如越界写操作)导致进程终止。
  • 对象状态破坏:绕过构造函数可能使对象处于不一致状态。

并发问题

  • 数据竞争:不当的 CAS 操作或内存读写引发线程安全问题。
  • 安全漏洞:恶意代码可利用 Unsafe 修改敏感数据(如密码字段)。

兼容性与官方态度

  • Java 9+ 限制:模块化系统禁止应用代码访问 sun.misc.Unsafe
  • 未来移除计划:JEP 471 提议弃用内存访问方法,推荐 jdk.incubator.foreign.MemorySegment

⚖️ 适用场景 vs 替代方案

场景推荐方案
堆外内存管理✅ Unsafe(高性能场景) → 替代:ByteBuffer.allocateDirectMemorySegment
无锁并发控制✅ Unsafe 的 CAS → 替代:AtomicIntegerLongAdder(已封装安全操作)
高性能框架开发✅ Netty、Disruptor 等底层依赖 → 普通应用无需直接使用
对象字段反射修改❌ 避免 Unsafe → 替代:标准反射 API 或 MethodHandles

💎 总结与最佳实践

  • 核心价值: Unsafe 是 Java 高性能生态的基石,为并发框架、网络库提供底层支持,但其能力与风险并存。
  • 使用原则:
    1. 仅限必要场景:如框架开发或极致性能优化。
  1. 严格资源管理:内存分配后必须配对释放(try-finally 块确保)。
  2. 升级替代方案:Java 17+ 优先使用 VarHandleMemorySegment 等安全 API。
  • 终极建议:

    “除非你清楚每一行代码的底层影响,否则远离 Unsafe。” —— 适用于 99% 的常规业务开发。

LockSupport

LockSupport 是 Java 并发包(java.util.concurrent.locks)中的线程阻塞唤醒工具类,提供基于许可(Permit)的底层线程控制机制。它是构建高级同步器(如 AQS、ReentrantLock)的基础,相比传统同步机制更灵活高效。以下从核心原理、关键方法、应用场景及底层实现展开详解:


⚙️ 核心原理:许可机制(Permit)

每个线程关联一个二元许可证(值仅为 01),默认初始值为 0

  • park():
    • 若许可为 1,消耗许可并立即返回
    • 若许可为 0阻塞线程,直到满足唤醒条件。
  • unpark(Thread thread):
    • 若目标线程许可为 0,将其置为 1
    • 若目标线程因 park() 阻塞,则唤醒它(唤醒后许可重置为 0)。

📌 关键特性

  • 顺序无关性unpark 可先于 park 调用(许可提前存储,park 不阻塞)。
  • 许可不累积:多次 unpark 仅保留一个许可。
  • 中断响应:阻塞线程被中断时,park() 自动返回(不抛异常),需手动检查 Thread.interrupted()

🔧 核心方法详解

方法作用
park()无限阻塞当前线程,直到被唤醒或中断。
park(Object blocker)阻塞线程并记录阻塞原因(blocker),便于监控工具诊断。
parkNanos(long nanos)阻塞指定纳秒数后自动返回(支持超时控制)。
parkUntil(long deadline)阻塞直到绝对时间戳 deadline(单位:毫秒)。
unpark(Thread thread)唤醒指定线程(精准控制,避免随机唤醒)。

💡 唤醒条件

  1. 其他线程调用 unpark(当前线程)
  2. 当前线程被中断(thread.interrupt());
  3. 虚假唤醒(极少发生)。

底层实现:跨越三层架构

LockSupport 的高效性源于其分层设计:

  1. Java API 层: 调用 Unsafe.park()/Unsafe.unpark() 的本地方法。
  2. JVM 层(HotSpot): 每个线程关联一个 Parker 对象(C++ 类),内含:
    • _counter:许可计数器(01);
    • _mutex_cond:基于 OS 的同步原语(如 Linux 的 pthread_cond_wait)。
  3. 操作系统层: Linux 通过 futex 系统调用实现高效线程阻塞/唤醒。
graph LR
A[Java: LockSupport.park()] --> B[JVM: Parker.park()]
B --> C[OS: futex/pthread_cond_wait]
C --> D[CPU: 线程挂起]

🆚 与传统同步机制对比

特性LockSupportObject.wait()/notify()Condition.await()/signal()
依赖锁对象❌ 无需任何锁✅ 需 synchronized✅ 需 ReentrantLock
唤醒精度✅ 指定线程唤醒notify() 随机唤醒signal() 随机唤醒
顺序容错性✅ 支持先 unparkpark❌ 先 notifywait 失效❌ 先 signalawait 失效
中断处理返回后需手动检查中断状态抛出 InterruptedException抛出 InterruptedException
使用复杂度低(无锁竞争风险)高(易死锁)中(需管理锁)

⚠️ Object.wait() 的局限: 必须在 synchronized 块中使用,且 notify() 无法精准唤醒特定线程,易导致“信号丢失”或“惊群效应”。


🛠️ 应用场景与示例

精准控制线程协作

// 示例:三个线程交替打印 ABC
public class AlternatePrint {
    static Thread t1, t2, t3;

    public static void main(String[] args) {
        Runnable taskA = () -> {
            for (int i = 0; i < 2; i++) {
                LockSupport.park();  // 等待唤醒
                System.out.print("A");
                LockSupport.unpark(t2); // 唤醒B
            }
        };
        // 类似实现 taskB(打印B后唤醒C)、taskC(打印C后唤醒A)
        t1 = new Thread(taskA);
        t2 = new Thread(taskB);
        t3 = new Thread(taskC);
        t1.start(); t2.start(); t3.start();
        LockSupport.unpark(t1); // 启动流程
    }
}
// 输出:ABCABC

超时控制与中断处理

Thread worker = new Thread(() -> {
    while (!workFinished()) {
        if (Thread.interrupted()) {
            cleanUp(); // 响应中断清理资源
            return;
        }
        LockSupport.parkNanos(500_000_000); // 阻塞0.5秒
    }
});
worker.start();
// 外部中断或超时后自动恢复

构建高级同步器(如AQS)

AQS 的阻塞队列依赖 LockSupport.park() 挂起线程,release() 时调用 unpark() 唤醒后继节点。


⚠️ 注意事项

  1. 避免死锁unpark() 需确保目标线程已启动(否则唤醒无效)。
  2. 许可不累积: 连续多次 unpark() 等效于一次,设计逻辑时勿依赖多次调用。
  3. 中断处理: 在 park() 后检查 Thread.interrupted(),确保业务逻辑健壮性。
  4. 阻塞原因记录: 使用 park(Object blocker) 便于诊断线程阻塞问题。

💎 总结

LockSupport 是 Java 并发编程的底层基石,通过许可机制精准唤醒解决了传统同步工具的局限性:

  • 灵活性:脱离锁束缚,可在任意位置阻塞/唤醒线程;
  • 高效性:基于 JVM 和 OS 原语,性能优于 synchronized
  • 精准性:指定线程唤醒,避免随机性和信号丢失;
  • 健壮性:支持超时控制和中断响应。 掌握 LockSupport 是理解 AQS、ReentrantLock 等高级同步器的关键,尤其适合构建高并发、低延迟的同步组件(如连接池、限流器)。

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer(AQS)是 Java 并发包(java.util.concurrent.locks)的核心框架,由 Doug Lea 设计,用于构建锁和其他同步器(如 ReentrantLockSemaphoreCountDownLatch 等)。其核心思想是通过 原子状态管理FIFO 线程等待队列 实现高效的线程同步。以下从设计原理、核心结构、工作流程、应用场景及实现案例展开详解:


🔧 核心设计原理

状态管理(State)

  • state 变量:
    volatile int
    
    类型,表示资源的可用状态(如锁是否被占用、信号量剩余许可数)。
    • 操作方法:
      • getState():获取当前状态。
    • setState(int):直接设置状态(非原子)。
      • compareAndSetState(int, int):CAS 原子更新状态。
    • 应用示例:
      • ReentrantLockstate=0 表示未锁定,state>0 表示锁定次数(支持重入)。
  • Semaphorestate 表示可用许可数量。

等待队列(CLH 变体)

  • 双向链表队列:存储等待资源的线程,节点为 Node对象
    • 关键字段:
      • head/tail:队首和队尾指针。
      • Node 结构:
      static final class Node { volatile int waitStatus; // 节点状态(如等待唤醒、已取消) volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 关联线程 Node nextWaiter; // 条件队列的下一个节点 }
    • waitStatus 状态值:
      • CANCELLED (1):线程已取消等待。
      • SIGNAL (-1):后续节点需被唤醒。
      • CONDITION (-2):节点处于条件队列(如 Condition 的等待队列)。

线程阻塞与唤醒

  • LockSupport 工具: 基于许可机制阻塞(park())或唤醒(unpark())线程,避免死锁风险。

⚙️ 工作流程(以独占模式为例)

获取资源(acquire

public final void acquire(int arg) {
    if (!tryAcquire(arg) && 
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}
  • 流程:
    1. tryAcquire(arg):子类实现,尝试获取资源(如 CAS 设置 state)。
    2. 失败则入队:
      • addWaiter():将线程封装为 Node 加入队尾(CAS 保证原子性)。
    • acquireQueued():自旋尝试获取资源,失败则阻塞(LockSupport.park())。
  1. 唤醒后重试:前驱节点释放资源后,唤醒当前节点重新尝试获取。

释放资源(release

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒后继节点
        return true;
    }
    return false;
}
  • 流程:
    1. tryRelease(arg):子类实现,释放资源(如重置 state)。
  1. 唤醒后继:若队列中存在有效节点,唤醒其线程。

📌 共享模式(如 Semaphore)逻辑类似,但允许多线程同时获取资源(tryAcquireShared() 返回剩余许可数)。


🛠️ AQS 的两种模式

模式特点应用场景
独占模式同一时间仅一个线程可获取资源(如 ReentrantLock写操作、互斥访问
共享模式多个线程可同时获取资源(如 SemaphoreCountDownLatch读操作、资源池管理

🌐 基于 AQS 的 JDK 同步工具

  1. ReentrantLock:
    • 独占锁,支持重入和公平性。
    • state 记录重入次数,公平锁按队列顺序获取资源。
  2. Semaphore:
    • 共享模式,state 表示可用许可数。
    • acquire() 减少许可,release() 增加许可。
  3. CountDownLatch:
    • 共享模式,初始化 state=NcountDown() 递减 stateawait()state=0 时唤醒线程。
  4. ReentrantReadWriteLock:
    • 混合模式:写锁独占,读锁共享。
    • state 高 16 位记录读锁数量,低 16 位记录写锁重入次数。

💻 自定义同步器实现

步骤

  1. 定义内部类继承 AQS,重写钩子方法(如 tryAcquiretryRelease)。
  2. 封装同步接口(如 Lock),委托给 AQS 子类执行。

示例:非重入互斥锁

public class Mutex implements Lock {
    private final Sync sync = new Sync();

    // 委托给 Sync 类
    @Override
    public void lock() { sync.acquire(1); }

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, 1); // CAS 获取锁
        }
        
        @Override
        protected boolean tryRelease(int arg) {
            setState(0); // 释放锁
            return true;
        }
    }
}

注意事项

  • 钩子方法需线程安全:避免使用阻塞操作。
  • 条件变量支持:通过 newCondition() 返回 ConditionObject(需实现 isHeldExclusively())。

⚖️ AQS 与 synchronized 对比

特性AQSsynchronized
可中断性✅ 支持 lockInterruptibly()❌ 不可中断
超时机制✅ 支持 tryLock(timeout, unit)❌ 不支持
公平性✅ 可配置(公平/非公平)❌ 仅非公平
实现复杂度需手动实现锁逻辑JVM 自动管理
性能高竞争场景更优(减少上下文切换)低竞争场景足够高效

💎 总结

AQS 通过 原子状态管理 + CLH 队列 + 模板方法 的设计,为构建高效同步组件提供了标准化框架:

  • 核心价值:解耦同步状态管理与线程调度,支持独占/共享两种模式。
  • 适用场景:高并发需精细控制锁(如超时、公平性)的场景。
  • 实践建议:优先使用 JDK 内置同步工具(如 ReentrantLock),仅当特殊需求时自定义 AQS 子类。

深入理解 AQS 是掌握 Java 并发的关键,其设计思想(如 CAS、队列调度)广泛用于高性能中间件(如数据库连接池、限流器)。

CLH 队列

CLH队列是一种基于链表的公平自旋锁实现,由Craig、Landin和Hagersten提出(取三人名字首字母命名)。它通过隐式队列管理线程的锁请求顺序,是Java并发框架中AbstractQueuedSynchronizer(AQS)的底层设计基础。以下从核心原理、实现机制、优缺点及应用场景展开详解:


⛓️ 核心结构与工作流程

核心组件

  • 节点(Node) 每个线程请求锁时创建一个节点,包含:
    • locked:布尔值,标识线程是否需要等待(true表示等待锁,false表示可获取锁)。
    • prev:指向前驱节点的引用(构建隐式链表)。
  • 尾指针(Tail) 原子引用类型(如AtomicReference),指向队列中最新加入的节点,通过CAS操作保证线程安全。

工作流程

graph LR
A[线程申请锁] --> B[创建新节点 locked=true]
B --> C[CAS操作加入队尾]
C --> D[自旋检查前驱节点状态]
D --> E{前驱节点 locked=false?}
E -- 是 --> F[获取锁执行]
E -- 否 --> D
F --> G[执行临界区代码]
G --> H[释放锁: locked=false]
H --> I[断开前驱引用]
  1. 加锁流程
    • 步骤1:线程创建新节点,locked=true
    • 步骤2:通过CAS将新节点置为队尾,并记录原尾节点作为前驱节点。
    • 步骤3:线程在前驱节点的locked字段上自旋等待,直到其变为false
  2. 释放流程
    • 步骤1:线程将自身节点的locked设为false,通知后继线程可获取锁。
    • 步骤2:断开当前节点与前驱节点的引用(避免内存泄漏)。

📌 示例

  • 线程A获取锁 → 节点A(locked=true)入队 → tail指向A。
  • 线程B加入 → 节点B入队 → tail指向B → B自旋检查A的locked
  • A释放锁:设置A.locked=false → B检测到后退出自旋,获取锁。

⚙️ 技术特点分析

核心优势

  • 严格公平性 按请求顺序分配锁(FIFO),彻底避免线程饥饿。
  • 局部自旋减少开销 线程仅自旋检查前驱节点的状态(而非全局变量),减少缓存同步和总线流量。
  • 无“惊群效应” 锁释放时仅唤醒直接后继节点,避免同时唤醒大量线程竞争资源。
  • 空间复杂度低 若有N个线程竞争L个锁,空间复杂度为O(L+N)

局限性

  • NUMA架构性能差 在非统一内存访问(NUMA)系统中,前驱节点可能位于远程内存,自旋检查延迟高。
  • 自旋消耗CPU 高竞争场景下,长时间自旋可能导致CPU空转(适用于短任务)。
  • 不直接支持锁重入 需额外机制实现重入(如AQS通过state计数支持)。

🔄 CLH在AQS中的变体实现

AQS对CLH进行了关键改进以适应更复杂场景:

  1. 双向链表代替单向链表 增加next指针,支持高效取消节点(如超时或中断)。
  2. 阻塞替代自旋LockSupport.park()挂起线程(非自旋),减少CPU消耗。
  3. 状态扩展节点
    waitStatus
    
    支持多种状态:
    • SIGNAL (-1):后继节点需唤醒。
    • CANCELLED (1):节点已取消。
    • CONDITION (-2):节点在条件队列等待。
  4. 双队列设计 分离同步队列​(锁竞争)和条件队列​(Condition等待),提升灵活性。

性能对比(CLH vs AQS变体)

场景CLH队列AQS变体
高竞争CPU空转严重线程挂起,减少开销
长任务不适用适用
公平性严格FIFO可配置公平/非公平

🌐 适用场景

  1. SMP架构的短任务锁 多核CPU共享内存的场景(如计数器累加)。
  2. 需严格公平性的场景 如交易系统按请求顺序处理。
  3. 基础锁原语设计 数据库事务管理、分布式锁(如ZooKeeper)参考CLH思想。

💎 总结

CLH队列通过隐式链表+前驱节点自旋的机制,以低开销实现了严格公平锁,尤其适合短任务SMP架构。其局限性(如NUMA性能差、CPU空转)促使AQS等框架引入阻塞唤醒状态扩展优化,成为Java高并发组件的基石。理解CLH是掌握现代同步器设计的关键一步。

Lock

Lock 接口是 Java 并发编程中用于替代 synchronized 的线程同步机制,位于 java.util.concurrent.locks 包中。它提供了更灵活、功能更丰富的锁控制能力,适用于复杂并发场景。以下从核心作用、方法详解、实现类、与 synchronized 的对比及适用场景展开分析:


🔒 Lock 接口的核心作用

  1. 精细化锁控制
    • 支持可中断锁超时获取锁非阻塞尝试锁等高级功能,弥补 synchronized 的不足。
  2. 显式锁管理
    • 需手动调用 lock()unlock() 方法,避免隐式锁的不可控性,但需在 finally 块中确保释放,防止死锁。
  3. 支持多条件变量
    • 通过 Condition 对象实现多条件队列(如 await()/signal()),比 synchronized 的单一等待集更灵活。

⚙️ Lock 接口方法详解

方法功能说明使用示例
void lock()阻塞获取锁,若锁被占用则线程休眠等待基础锁操作,需配合 unlock() 使用
boolean tryLock()尝试非阻塞获取锁,成功返回 true,失败立即返回 false适用于轻量级任务,避免线程阻塞
tryLock(long, TimeUnit)超时尝试获取锁,支持中断响应避免无限等待,设置超时时间(如 lock.tryLock(100, TimeUnit.MILLISECONDS)
lockInterruptibly()可中断获取锁,等待过程中响应 interrupt() 信号并抛出 InterruptedException解决线程长期阻塞问题
void unlock()释放锁,必须finally 块中调用确保异常时锁仍释放
Condition newCondition()创建绑定到锁的条件变量,支持精细线程通信替代 wait()/notify(),实现多条件等待
// 标准使用模板(避免死锁)
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 同步代码逻辑
} finally {
    lock.unlock(); // 确保释放
}

🔧 Lock 的主要实现类

  1. ReentrantLock(可重入锁)
    • 特性:
      • 同一线程可重复获取锁(重入计数)。
      • 支持公平锁(按请求顺序分配)和非公平锁(默认,吞吐量高)。
    • 适用场景:替代 synchronized,需重入或公平性的场景。
  2. ReentrantReadWriteLock(读写锁)
    • 特性:
      • 分离读锁(共享)和写锁(独占),读多写少时大幅提升并发性能。
      • 写锁优先:避免读线程饥饿。
    • 示例:
      ReadWriteLock rwLock = new ReentrantReadWriteLock();
      rwLock.readLock().lock();  // 多线程可并发读
      rwLock.writeLock().lock(); // 写锁独占
      

↔️ Lock vs Synchronized 核心对比

特性LockSynchronized
锁机制显式锁(手动获取/释放)隐式锁(自动释放)
可中断性✅ 支持 lockInterruptibly()❌ 不可中断
超时机制tryLock(timeout)❌ 不支持
公平性✅ 可配置(公平/非公平)❌ 仅非公平锁
条件变量✅ 支持多个 Condition❌ 单一等待集
性能(高竞争)更优(CAS 减少阻塞)JDK 6+ 优化后接近
死锁风险需手动释放(忘记 unlock() 易死锁)自动释放(异常时安全)
代码复杂度较高(需模板代码)简洁(关键字修饰)

选择原则

  • 简单同步synchronized(代码简洁)。
  • 高级需求(可中断、超时、公平锁) → Lock

🎯 适用场景

  1. 高并发读场景 使用 ReentrantReadWriteLock 提升读并发性能(如缓存系统)。
  2. 避免线程无限阻塞 需超时(tryLock)或可中断(lockInterruptibly)的敏感任务(如网络请求)。
  3. 复杂线程协作 多条件变量(Condition)实现精细调度(如生产者-消费者模型)。
  4. 公平性要求 需按请求顺序分配资源(如交易系统)。

⚠️ 注意事项

  1. 锁释放:务必在 finally 中调用 unlock(),否则可能导致死锁。
  2. 性能权衡:低竞争时 synchronized 性能接近,且代码更简洁。
  3. ABA 问题Lock 本身不解决,需结合 AtomicStampedReference

💎 总结

Lock 接口通过显式锁管理可中断性超时控制多条件变量,提供了比 synchronized 更灵活的并发控制能力。其核心实现类 ReentrantLockReentrantReadWriteLock 分别适用于重入锁和读写分离场景。选择时需权衡:

  • 简单需求synchronized(安全简洁)。
  • 复杂控制Lock(功能强大,需谨慎释放锁)。

Condition

以下是关于 Java 并发编程中 Condition 接口的详细解析,涵盖其核心原理、使用规范、底层实现及典型应用场景,结合权威技术文档与源码实现整理:


⚙️ Condition 的核心作用与优势

Conditionjava.util.concurrent.locks 包中与显式锁(如 ReentrantLock)配套的线程协调机制,用于替代传统的 Object.wait()/notify(),提供更精细化的线程等待/通知控制

  1. 多条件队列:
    • 一个 Lock 可关联多个 Condition(例如生产者-消费者模型中的“非空”和“非满”条件),每个条件独立管理线程等待队列。
    • 示例:

ReentrantLock lock = new ReentrantLock(); Condition notEmpty = lock.newCondition(); // 队列非空条件 Condition notFull = lock.newCondition(); // 队列非满条件 ``` 2. 精准唤醒:

  • 支持 signal()(唤醒单个等待线程)和 signalAll()(唤醒全部线程),避免 notifyAll() 导致的无效竞争。
  1. 增强控制:
    • 提供超时等待(awaitNanos())、不可中断等待(awaitUninterruptibly())、截止时间等待(awaitUntil())等灵活选项。

⛓️ 底层实现原理(基于 AQS)

Condition 的实现类 ConditionObject 是 AQS(AbstractQueuedSynchronizer)的内部类,核心机制如下:

  1. 双队列模型:
    • 同步队列:AQS 的主队列,管理竞争锁的线程。
    • 条件队列:每个 Condition 维护独立的 FIFO 队列,存储等待特定条件的线程节点(Node),节点状态为 CONDITION(-2)。
  2. await() 流程:
  • 释放当前线程持有的锁(完全释放,即使重入锁也会清零计数)。
    • 创建 CONDITION 节点加入条件队列尾部。
    • 阻塞线程,等待被唤醒或中断。
    • 唤醒后,节点从条件队列转移到同步队列,重新竞争锁。
  1. signal() 流程:
    • 将条件队列的头节点转移到同步队列。
    • 修改节点状态,并通过 LockSupport.unpark() 唤醒线程(唤醒后仍需竞争锁)。

📌 关键设计:条件队列与同步队列分离,通过节点迁移实现锁释放与重新获取的原子性,避免信号丢失。


⚠️ 使用规范与注意事项

强制使用规范

规则说明违规后果
先获取锁调用 await()/signal() 前必须持有关联的 LockIllegalMonitorStateException
循环检查条件必须用 while 而非 if 检查条件,防止虚假唤醒(Spurious Wakeup)逻辑错误(条件不满足时错误执行)
确保释放锁unlock() 必须放在 finally 块中死锁风险

中断处理

  • await() 可能抛出 InterruptedException,需明确处理中断逻辑(如重试或终止任务)。
  • awaitUninterruptibly() 提供不可中断模式,适用于必须完成的任务。

唤醒策略建议

  • 优先 signal():明确唤醒目标时使用,减少线程竞争开销。
  • 谨慎 signalAll():不确定唤醒目标时使用,但可能引发“惊群效应”。

🛠️ 典型应用场景与案例

生产者-消费者模型(多 Condition 版)

public class BoundedBuffer<T> {  
    private final Queue<T> buffer;  
    private final int capacity;  
    private final ReentrantLock lock = new ReentrantLock();  
    private final Condition notFull = lock.newCondition();  // 队列非满条件  
    private final Condition notEmpty = lock.newCondition(); // 队列非空条件  

    public void put(T item) throws InterruptedException {  
        lock.lock();  
        try {  
            while (buffer.size() == capacity) {  
                notFull.await();  // 队列满时等待  
            }  
            buffer.add(item);  
            notEmpty.signal();    // 唤醒一个消费者  
        } finally {  
            lock.unlock();  
        }  
    }  

    public T take() throws InterruptedException {  
        lock.lock();  
        try {  
            while (buffer.isEmpty()) {  
                notEmpty.await(); // 队列空时等待  
            }  
            T item = buffer.remove();  
            notFull.signal();     // 唤醒一个生产者  
            return item;  
        } finally {  
            lock.unlock();  
        }  
    }  
}  

优势

  • 生产者仅唤醒消费者(而非所有线程),减少无效竞争。

多任务顺序调度(精准唤醒)

class TaskScheduler {  
    private final ReentrantLock lock = new ReentrantLock();  
    private final Condition condA = lock.newCondition();  
    private final Condition condB = lock.newCondition();  
    private int flag = 1; // 1:执行A, 2:执行B  

    public void runTaskA() throws InterruptedException {  
        lock.lock();  
        try {  
            while (flag != 1) {  
                condA.await();    // 仅等待条件A  
            }  
            // 执行任务A  
            flag = 2;  
            condB.signal();       // 精准唤醒任务B  
        } finally {  
            lock.unlock();  
        }  
    }  
    // runTaskB 类似,唤醒 condA  
}  

适用场景:多阶段任务(如加油→洗车→驶离)的顺序控制。


🔄 Condition vs Object 监视器方法

特性Lock + Conditionsynchronized + wait/notify
条件变量数量✅ 多条件队列(可自定义)❌ 单一等待队列
超时控制✅ 支持 awaitNanos(), awaitUntil()⚠️ 仅有限超时(wait(long timeout)
中断响应✅ 支持可中断/不可中断模式❌ 仅抛异常
唤醒精度✅ 可精准唤醒指定条件队列❌ 只能唤醒全部(notifyAll()
锁机制显式锁(代码控制)隐式锁(JVM 管理)

💡 选型建议

  • 简单同步 → synchronized(代码简洁)。
  • 复杂线程协作(如多条件、超时控制)→ Lock + Condition

⚠️ 工程实践注意事项

  1. 避免嵌套调用:
    • 禁止在 Condition 等待中嵌套调用其他锁操作,防止死锁。
  2. 性能敏感场景:
    • 高并发队列(如 ArrayBlockingQueue)底层采用 Condition 实现,优先复用标准库组件。
  3. 监控工具:
    • 使用 jstackJProfiler 查看条件队列状态,定位线程阻塞问题。

💎 总结

Condition 通过多条件队列、精准唤醒和灵活等待机制,实现了比传统 wait/notify 更精细的线程协作,尤其适用于生产者-消费者、任务调度等复杂同步场景。其底层依赖 AQS 的双队列模型和节点迁移机制,保证了操作的原子性。使用时需严守锁持有、循环检查条件和中断处理三大规范,避免死锁与逻辑错误。在 Java 并发编程中,Condition 是构建高效、可控同步逻辑的核心工具之一。

本文技术要点源自 AQS 源码、《Java并发编程实战》及生产环境最佳实践。

Semaphore

🚦 Semaphore(信号量)详解:原理、实现与应用场景

Semaphore 是 Java 并发包(java.util.concurrent)中的核心同步工具,用于控制对共享资源的并发访问数量,通过维护“许可证”(Permits)实现线程数量的精确限制。以下是其全方位解析:


⚙️ 核心原理与模型

信号量模型

  • 计数器机制: Semaphore 内部维护一个整数计数器(state),表示可用许可证数量:
    • acquire()(P操作):请求许可证,计数器减 1;若计数器为 0,则线程阻塞。
    • release()(V操作):释放许可证,计数器加 1,唤醒等待线程。
  • 经典类比: 停车场模型(3 个车位)→ 5 辆车同时到达,仅 3 辆可进入,后续车辆需等待空位。

公平性与非公平性

  • 非公平模式(默认): 线程直接尝试获取许可证,不保证顺序(高吞吐量)。
  • 公平模式: 线程按 FIFO 顺序获取许可证(通过 AQS 队列实现)。 构造函数示例:new Semaphore(5, true)

⚙️ 底层实现(基于 AQS)

AQS 共享模式

Semaphore 通过内部类 Sync(继承自 AbstractQueuedSynchronizer)实现:

  • state 字段:存储可用许可证数量。
  • 核心方法:
    • 非公平获取:nonfairTryAcquireShared()
    int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; // 负数表示获取失败 } }
  • 公平获取tryAcquireShared() 中先检查是否有前驱节点(hasQueuedPredecessors())。

线程阻塞与唤醒

  • acquire() → 调用 sync.acquireSharedInterruptibly(1): 若 tryAcquireShared() 返回负值,线程加入 AQS 队列阻塞。
  • release() → 调用 sync.releaseShared(1): 通过 CAS 增加 state,并唤醒队列中的后继节点。

🛠️ 核心 API 与使用示例

常用方法

方法作用
acquire()阻塞获取一个许可
tryAcquire()尝试获取许可(立即返回成功/失败)
tryAcquire(long timeout, TimeUnit unit)超时等待获取许可
release()释放一个许可
availablePermits()返回当前可用许可数

代码示例:限制并发下载线程数

// 允许最多 3 个线程同时下载
Semaphore semaphore = new Semaphore(3);

void downloadFile(String url) throws InterruptedException {
    semaphore.acquire(); // 获取许可
    try {
        // 模拟下载耗时
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + " 下载完成: " + url);
    } finally {
        semaphore.release(); // 确保释放许可
    }
}
// 启动 10 个下载线程(仅 3 个并发执行)
for (int i = 0; i < 10; i++) {
    new Thread(() -> downloadFile("file" + i)).start();
}

🌐 应用场景

资源池管理

  • 数据库连接池:限制最大连接数(如最多 10 个连接)。
  • 对象池:复用昂贵资源(如线程、网络连接)。
    public class ObjectPool<T> {
        private final Semaphore semaphore;
        private final BlockingQueue<T> pool;
    
        public ObjectPool(int size, Supplier<T> supplier) {
            semaphore = new Semaphore(size);
            pool = new LinkedBlockingQueue<>();
            for (int i = 0; i < size; i++) {
                pool.add(supplier.get());
            }
        }
    
        public T borrow() throws InterruptedException {
            semaphore.acquire();
            return pool.take();
        }
    
        public void release(T obj) {
            pool.offer(obj);
            semaphore.release();
        }
    }
    

流量控制

  • API 限流:限制每秒请求外部服务的次数(如每秒 100 次)。
  • 下载限速:控制同时下载文件的用户数(如最多 3 个).

生产者-消费者协调

  • 缓冲队列:通过 Semaphore 控制生产速率(如生产前检查空闲缓冲区).

⚠️ 注意事项

  1. 许可释放匹配: 确保每个 acquire() 都有对应的 release(),否则会导致资源泄漏或死锁。
  2. 避免超额释放release() 次数 > acquire() 次数会抛出 SemaphoreFullException
  3. 性能选择:
    • 单进程优先用 SemaphoreSlim(.NET 中的轻量级实现,用户态同步)。
    • 跨进程同步需用带名称的 Semaphore(内核级同步).
  4. 超时控制: 高并发场景使用 tryAcquire(timeout) 避免线程无限阻塞.

💎 总结

  • 核心价值:Semaphore 通过许可证计数实现对并发线程数的精确控制,适用于资源池、限流等高并发场景。
  • 选型建议:
    • 简单限流 → Semaphore
  • 高性能单进程 → SemaphoreSlim(其他语言)或非公平模式;
    • 严格顺序 → 公平模式。
  • 底层基石:基于 AQS 的共享模式实现,结合 CAS 和队列管理保证高效与安全。

通过合理使用 Semaphore,可显著提升系统的稳定性和资源利用率,是并发编程中的“流量阀门”。

CountDownLatch

CountDownLatch 是 Java 并发包(java.util.concurrent)中的核心同步工具类,用于协调多个线程的执行顺序,允许一个或多个线程等待其他线程完成操作后再继续执行。其设计基于计数器机制,结合 AQS(AbstractQueuedSynchronizer)实现高效线程阻塞与唤醒。以下从核心原理、使用方式、应用场景、源码实现及对比分析展开详解:


⚙️ 核心机制:计数器模型

核心组件

  • 计数器(State)初始化时指定正整数 count,表示需等待完成的线程/任务数量。
    • countDown():任务完成后调用,计数器减 1(线程安全,基于 CAS)。
  • await():阻塞当前线程,直到计数器归零(支持超时和中断响应)。

一次性特性

计数器归零后无法重置,若需重复使用,需换用 CyclicBarrier


📝 使用方式与示例

基础使用步骤

// 1. 初始化计数器(假设需等待3个任务)
CountDownLatch latch = new CountDownLatch(3);

// 2. 启动子线程(每个线程完成任务后调用 countDown())
new Thread(() -> {
    doTask();
    latch.countDown(); // 任务完成,计数器-1
}).start();

// 3. 主线程等待所有子线程完成
latch.await(); 
System.out.println("所有任务已完成!");

典型场景示例

  • 场景1:主线程等待所有子线程完成任务 主线程启动多个子线程执行任务,汇总最终结果(如批量文件处理)。
    ExecutorService executor = Executors.newFixedThreadPool(5);
    CountDownLatch latch = new CountDownLatch(5);
    for (int i = 0; i < 5; i++) {
        executor.submit(() -> {
            try {
                doWork();
            } finally {
                latch.countDown(); // 确保异常时仍释放计数器
            }
        });
    }
    latch.await(); // 主线程阻塞等待
    executor.shutdown();
    
  • 场景2:并发测试(模拟同时触发) 多个线程等待统一信号后同时执行(如模拟秒杀请求)。
    CountDownLatch startSignal = new CountDownLatch(1); // 发令枪
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            startSignal.await(); // 等待发令
            executeConcurrentTask();
        }).start();
    }
    Thread.sleep(2000);
    startSignal.countDown(); // 同时释放所有线程
    

🌐 应用场景

场景类型说明示例
主从协作主线程需等待所有子任务完成后再继续执行服务启动时等待资源初始化完成
阶段同步多阶段任务中,下一阶段需等待前一阶段所有子任务结束分批次数据处理后汇总结果
并发压力测试模拟高并发场景,控制多个线程同时触发请求接口秒杀性能测试
资源依赖检查确保所有依赖资源(如数据库连接、配置加载)就绪后再执行业务逻辑分布式系统启动协调

🔧 底层实现(基于 AQS)

核心源码解析

  • 内部类 Sync 继承 AQS:
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) { setState(count); } // 初始化计数器
    
        // 尝试获取共享锁:计数器为0时返回1(成功),否则阻塞
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
    
        // 释放共享锁:CAS 减少计数器,归零时唤醒等待线程
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0) return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0; // 计数器归零时返回 true
            }
        }
    }
    

关键方法映射

CountDownLatch 方法AQS 操作作用
await()acquireSharedInterruptibly(1)阻塞线程,直到计数器归零
countDown()releaseShared(1)减少计数器,归零时唤醒所有阻塞线程

💡 共享模式优势: 允许多个线程同时等待计数器归零,唤醒时所有等待线程并发执行(对比独占模式效率更高)。


⚠️ 注意事项

  1. 不可重用性: 计数器归零后无法重置,重复使用需新建实例。
  2. 异常处理: 子线程中务必在 finally 中调用 countDown(),避免任务异常导致主线程永久阻塞。
  3. 超时控制: 使用 await(long timeout, TimeUnit unit) 避免死锁,超时返回 false 后可执行降级逻辑。
  4. 性能影响: 高并发场景下,await() 阻塞线程可能增加上下文切换开销,需结合线程池优化。

⚖️ 与其他同步工具对比

特性CountDownLatchCyclicBarrierSemaphore
重置性❌ 一次性✅ 可循环使用✅ 动态调整许可数
等待方向主等子子等子(相互等待)控制资源访问数量
核心方法await()/countDown()await()acquire()/release()
适用场景任务完成协调分阶段任务同步资源池限流

选型建议

  • 等待离散事件完成 → CountDownLatch
  • 多线程分阶段汇聚 → CyclicBarrier
  • 限制并发资源访问 → Semaphore

💎 总结

CountDownLatch 是 Java 并发编程中的线程协调利器,通过简洁的计数器模型解决“主等子”场景的同步问题:

  • 核心价值:解耦任务执行与结果汇总,提升多线程协作效率;
  • 最佳实践:结合线程池管理资源,严格处理异常与超时;
  • 适用边界:一次性任务协调,需重复同步时换用 CyclicBarrier

深入理解其基于 AQS 的共享模式实现,可更好地驾驭高并发场景下的线程调度(如分布式系统启动、批量任务处理)。

CyclicBarrier

以下是关于 Java 并发工具 CyclicBarrier 的详细解析,结合其核心原理、源码实现、使用场景及与同类工具的对比展开说明:


⚙️ 核心原理:循环屏障机制

核心设计

  • 屏障点(Barrier Point) 初始化时指定线程数量 parties,每个线程调用 await() 表示到达屏障点并阻塞。当第 parties 个线程到达时,屏障打开,所有线程同时继续执行。
  • 可重用性(Cyclic) 屏障触发后自动重置计数器(count = parties),支持多轮同步,无需重新创建实例。
  • 屏障动作(Barrier Action) 可选的回调任务(Runnable),在所有线程唤醒前由最后一个到达的线程执行,用于汇总结果或发令。

工作流程

graph LR
A[线程调用 await] --> B{当前是第 parties 个线程?}
B -- 是 --> C[执行 Barrier Action]
C --> D[唤醒所有线程 & 重置屏障]
B -- 否 --> E[线程阻塞等待]
D --> F[所有线程继续执行]

⛓️ 底层实现:基于锁与条件变量

核心组件(源码关键字段)

  • ReentrantLock lock 保证线程安全的计数器操作和条件等待。
  • Condition trip 线程阻塞队列,未到达屏障的线程在此等待。
  • Generation generation 代际管理对象,记录当前屏障状态(是否损坏 broken)。每次屏障重置时创建新代,确保异常不影响下一轮。

await() 执行步骤

  1. 获取锁:通过 lock.lock() 进入临界区。
  2. 检查状态:若屏障已损坏(generation.broken),抛出 BrokenBarrierException
  3. 计数器减1int index = --count
  4. 触发屏障:若
   index == 0
   ```:
   - 执行 `barrierCommand.run()`(若有)。
   - 调用 `nextGeneration()`:唤醒所有线程、重置计数器、创建新代。
5. **阻塞等待**:非最后到达的线程进入 `trip.await()` 挂起。
> 📌 **重置逻辑(`nextGeneration()`)**:
>
> ```
> private void nextGeneration() {
>   trip.signalAll();          // 唤醒所有线程
>   count = parties;           // 重置计数器
>   generation = new Generation(); // 创建新代
> }
> ```


------
### 🛠️ **使用场景与最佳实践**

#### **典型应用场景**

- 多阶段任务同步
如并行计算中,每阶段结束后线程需同步数据后再进入下一阶段。

// 分阶段计算示例 CyclicBarrier barrier = new CyclicBarrier(THREAD_NUM); for (int stage = 0; stage < STAGES; stage++) { for (Thread thread : threads) { thread.execute(() -> { computeStage(stage); barrier.await(); // 等待所有线程完成本阶段 }); } }

- **高并发测试**
模拟瞬时高流量(如秒杀场景),控制所有线程同时发起请求。
- **资源初始化**
多个依赖资源并行初始化,全部就绪后启动服务。
#### **最佳实践与避坑指南**

- **超时控制**
使用 `await(long timeout, TimeUnit unit)` 避免线程永久阻塞(抛出 `TimeoutException`)。
- **异常处理**
线程中断或超时会导致屏障损坏(`broken=true`),其他线程唤醒时将抛出 `BrokenBarrierException`。
- **屏障动作轻量化**
`barrierCommand` 应快速执行,否则阻塞所有线程。
- **重置谨慎性**
`reset()` 方法强制重置屏障,可能中断等待中的线程。


------
### ⚖️ **与 CountDownLatch 的对比**

| **特性**     | **CyclicBarrier**                         | **CountDownLatch**                 |
| ------------ | ----------------------------------------- | ---------------------------------- |
| **重用性**   | ✅ 自动重置,支持多轮同步                  | ❌ 一次性使用                       |
| **等待方向** | 子线程互相等待(对等协作)                | 主线程等待子线程(主从协作)       |
| **计数器**   | 加法计数(`parties` 初始化,递减至0触发) | 减法计数(初始化后递减,归零触发) |
| **回调支持** | ✅ 支持 `barrierAction`                    | ❌ 无                               |
| **适用场景** | 多阶段任务、循环同步                      | 单次任务完成等待(如服务启动)     |
> **示例区分**:
>
> - 赛跑场景:
>
>   - `CyclicBarrier` 同步起跑(所有选手就绪后同时开始)。
>- `CountDownLatch` 等待所有选手完赛(主线程统计结果)。


------
### 💎 **总结**

- **核心价值**:
`CyclicBarrier` 通过 ​**可重用屏障**​ 和 ​**多线程对等同步**​ 机制,解决了分阶段任务协作问题,尤其适合迭代式并行计算。
- 选型建议:
- 需循环同步 → `CyclicBarrier`;
- 单次等待 → `CountDownLatch`;
- 资源限流 → `Semaphore`。
- **性能注意**:
高竞争场景下,基于锁的实现可能成为瓶颈,可考虑无锁方案(如 `Phaser`)。
> 通过合理使用 `CyclicBarrier`,可显著提升多线程协作的简洁性与健壮性,是构建高并发系统的核心同步工具之一。
Licensed under CC BY-NC-SA 4.0
Last updated on Jul 15, 2025 01:02 CST
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy