在高并发编程中,混合使用CAS(Compare-And-Swap)和传统锁机制是一种非常成功的策略,它能在保证线程安全的同时,显著提升性能。下面通过几个经典案例来说明这种混合模式的巧妙之处。
🔒 案例一:ConcurrentHashMap(JDK 1.8+)
JDK 1.8 中的 ConcurrentHashMap
是混合使用 CAS 和 synchronized
锁的典范。
-
CAS 的应用场景:
- 初始化和扩容控制 :在初始化哈希表数组或判断是否需要扩容时,会操作一个名为
sizeCtl
的变量。代码通过 CAS 操作来原子性地更新sizeCtl
的值,例如尝试将其从一个状态设置为扩容标志。如果 CAS 失败,说明有其他线程已经在进行扩容操作,当前线程便会协助扩容,而不是重复操作或阻塞等待。这避免了使用重量级锁带来的开销。 - 空桶插入:当向一个空的哈希桶(数组位置)插入新节点时,代码会先使用 CAS 操作尝试将新节点设置到该位置。如果 CAS 成功,则表示插入完成,无需加锁。这在高并发环境下,当哈希冲突不严重时,极大地提升了插入速度。
- 初始化和扩容控制 :在初始化哈希表数组或判断是否需要扩容时,会操作一个名为
-
synchronized 锁的应用场景:
- 当发生哈希冲突,需要向非空的桶(可能是链表或红黑树)进行插入、更新或删除操作时,
ConcurrentHashMap
会使用synchronized
关键字锁定这个桶的头节点。锁的粒度非常细,只针对当前操作的桶,其他桶的读写操作不受影响。这种细粒度锁确保了在存在竞争的情况下,对链表或树结构修改的线程安全。
- 当发生哈希冲突,需要向非空的桶(可能是链表或红黑树)进行插入、更新或删除操作时,
这种设计的好处是:在低竞争 (如多数桶为空)情况下,利用无锁的 CAS 获得接近无锁的高性能;在高竞争(如操作同一个桶)情况下,通过细粒度的同步锁保证数据一致性,实现了性能与安全的平衡。
📊 案例二:LongAdder
LongAdder
是针对高并发计数场景设计的类,它通过一种称为"分段"的思想混合了 CAS 和类似锁的竞争机制。
- 核心思想 :当多个线程同时更新一个计数器时,如果只用一个变量(如
AtomicLong
),CAS 竞争会非常激烈,导致大量线程不断重试。LongAdcedr
的内部维护了一个基准值(base
)和一个动态的单元数组(Cell[]
)。 - CAS 的应用 :当线程要增加数值时,首先会尝试使用 CAS 操作更新
base
值。如果成功,操作就完成了。 - "分段"锁思想的体现 :如果线程在更新
base
时 CAS 失败,说明发生了竞争。它不会一直自旋,而是会根据自己的哈希值映射到Cell
数组中的某个单元,然后尝试对这个单元内的变量进行 CAS 更新。这样,就将对单一热点的竞争分散到了多个单元上。 - 退避策略 :如果对某个
Cell
的 CAS 操作也失败了,LongAdder
并不会让线程无限重试,而是会尝试扩容Cell
数组,进一步分散竞争。这类似于一种轻量的"锁升级"策略,通过扩大资源来减少冲突。
最终,获取总值时,只需将 base
和所有 Cell
的值累加即可。这种"分散竞争"的策略,使得 LongAdder
在高并发写场景下的性能远高于 AtomicLong
,是典型的以空间换时间的成功案例。
🔄 案例三:锁升级机制(JVM层面)
Java 虚拟机(JVM)内部的 synchronized 锁优化策略------锁升级,本身就是一种根据竞争强度动态混合无锁、CAS 和重量级锁的机制。
- 偏向锁(可视为一种乐观无锁优化):初期,锁会偏向于第一个获得它的线程。之后该线程再进入同步块时,无需任何同步操作(如 CAS),仅仅检查对象头的标记即可,开销极小。这适用于几乎没有锁竞争的场景。
- 轻量级锁(主要依赖 CAS) :当有第二个线程尝试获取锁时,偏向锁升级为轻量级锁。线程会通过 CAS 操作尝试在栈帧中创建锁记录(Lock Record)并更新对象头。如果成功,则获得锁;如果失败,则会自旋重试一定次数。
- 重量级锁(传统互斥锁):如果自旋重试超过一定次数(或等待线程过多),锁会升级为重量级锁。未能获取锁的线程会被挂起,进入阻塞队列,等待操作系统的调度唤醒。这时,synchronized 就表现为一个真正的互斥锁。
这个过程完美体现了混合策略的精髓:根据实时竞争情况,从开销最小的方案(偏向锁)平滑过渡到最安全的方案(重量级锁),在绝大多数时间避免了昂贵的线程阻塞和唤醒。
💎 总结与模式提炼
从这些成功案例中,我们可以提炼出混合使用 CAS 和锁的通用模式:
策略 | 核心思想 | 适用场景 | 案例体现 |
---|---|---|---|
CAS 探路,锁保安全 | 先尝试低开销的 CAS,失败后再使用锁。 | 常见路径无竞争,但可能存在冲突。 | ConcurrentHashMap 的空桶插入 |
分散竞争,分而治之 | 将单一竞争点拆分为多个,降低冲突概率。 | 对单一热点资源的高频写操作。 | LongAdder 的分段计数 |
动态升级,按需适配 | 根据竞争强度,从轻量级方案平滑过渡到重量级方案。 | 竞争强度难以预测或会动态变化。 | JVM 的锁升级机制 |