CAS (Compare and Swap) 笔记251007
好的,我们来详细、深入地解析一下 CAS(Compare and Swap)。
CAS 是现代并发编程的基石之一,它使得无锁(Lock-Free)和高性能的并发算法成为可能。
1. 什么是 CAS?
CAS 是一种原子操作 。它用于在多线程环境下实现同步,而无需使用重量级的锁(如 synchronized
或 ReentrantLock
)。
它的核心思想是:"我认为内存位置 V 的值应该是 A,如果是,那么把它更新为 B;否则,不要修改它,但要告诉我当前的实际值是什么。"
一个 CAS 操作包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
CAS 的语义可以用以下伪代码来表示:
typescript
boolean CAS(V, A, B) {
if (V == A) { // 只有在内存值等于预期原值时,才进行更新
V = B
return true // 表示更新成功
} else {
return false // 表示更新失败
}
}
关键在于,整个比较和交换的过程是一个单一的、不可中断的原子操作 。CPU 提供了一条特殊的指令(在 x86 架构上是 CMPXCHG
)来保证这一点。
2. CAS 的工作原理
当多个线程同时尝试对同一个内存位置进行 CAS 操作时,会发生什么?
- 线程 1 和线程 2 都从主内存中读取变量
V
,假设值为100
。 - 它们都打算将
100
增加1
,所以它们都计算出新值101
。 - 线程 1 率先执行 CAS 操作:
- 它检查
V
的值是否还是它之前读到的100
。 - 此时确实是
100
,所以 CAS 成功。V
被更新为101
。CAS(V, 100, 101)
返回true
。
- 它检查
- 线程 2 紧接着执行 CAS 操作:
- 它检查
V
的值是否还是它之前读到的100
。 - 但此时
V
已经被线程 1 改为101
了。 - 值不匹配,所以 CAS 失败。
CAS(V, 100, 101)
返回false
。
- 它检查
- 线程 2 的 CAS 操作失败后,它并不会阻塞或挂起 。典型的做法是重试(自旋) :
- 它重新读取
V
的当前值(现在是101
)。 - 重新计算新值(
101 + 1 = 102
)。 - 再次发起 CAS 操作:
CAS(V, 101, 102)
。 - 这次因为值匹配,所以很可能成功。
- 它重新读取
这个过程就像在说:"我先看看现在是多少,算好新值,然后尝试更新。如果有人在我之前改动了,那我就再试一次。"
3. CAS 在 Java 中的实现
在 Java 中,CAS 操作是通过 sun.misc.Unsafe
类中的本地(Native)方法实现的。但我们通常不会直接使用 Unsafe
类,而是使用 JDK 封装好的原子类。
最经典的例子就是 java.util.concurrent.atomic.AtomicInteger
。
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
public static void main(String[] args) {
AtomicInteger atomicInt = new AtomicInteger(100);
// 模拟线程1
boolean success1 = atomicInt.compareAndSet(100, 101); // CAS 操作
System.out.println("Thread 1 CAS: " + success1 + ", value: " + atomicInt.get()); // true, 101
// 模拟线程2
boolean success2 = atomicInt.compareAndSet(100, 101); // CAS 操作,但预期值已是旧的100
System.out.println("Thread 2 CAS: " + success2 + ", value: " + atomicInt.get()); // false, 101
// 线程2重试
boolean success3 = atomicInt.compareAndSet(101, 102); // 使用新读到的值101作为预期值
System.out.println("Thread 2 Retry CAS: " + success3 + ", value: " + atomicInt.get()); // true, 102
}
}
输出:
yaml
Thread 1 CAS: true, value: 101
Thread 2 CAS: false, value: 101
Thread 2 Retry CAS: true, value: 102
compareAndSet
方法就是 Java 中 CAS 操作最直接的体现。
4. CAS 的典型应用场景
- 无锁计数器 :如上文的
AtomicInteger
,实现线程安全的递增递减。 - 乐观锁:在数据库和并发控制中非常常见。例如,更新数据前先读取一个版本号,更新时检查版本号是否变化,没变化则更新,变化了则重试或报错。
- 实现复杂的无锁数据结构 :如
ConcurrentLinkedQueue
(无锁队列)、ConcurrentHashMap
(在 JDK 1.8 的部分实现中使用了 CAS)等高性能并发容器。
5. CAS 的优缺点
优点:
-
高性能:
- 避免了线程阻塞和上下文切换的开销。在低到中度锁竞争的情况下,性能远高于传统的锁。
- 线程在 CAS 失败后立即重试,响应速度快。
-
避免死锁:因为不使用锁,所以从根本上避免了死锁问题。
缺点:
-
ABA 问题:
- 这是 CAS 最著名的陷阱。
- 描述:一个变量原来是 A,被线程 1 读取。之后,线程 2 将其修改为 B,然后又修改回 A。此时,线程 1 执行 CAS 操作,发现值还是 A,于是误以为它没有被修改过,CAS 成功。
- 在某些场景下这是有问题的,例如一个无锁栈,如果头节点经历了 A->B->A 的变化,虽然值没变,但整个栈的结构可能已经变了。
- 解决方案 :使用版本号/标记 。每次变量更新时,版本号都递增。Java 提供了
AtomicStampedReference
和AtomicMarkableReference
来解决 ABA 问题。
-
循环时间长开销大:
- 如果 CAS 操作长时间不成功(竞争非常激烈),CPU 会一直进行自旋,消耗大量 CPU 资源。
- 在这种情况下,传统的锁(让竞争失败的线程挂起)可能反而是更好的选择。
-
只能保证一个共享变量的原子操作:
- 单个 CAS 操作只能针对一个变量。如果你需要对多个变量进行原子操作,就比较麻烦。
- 解决方案 :可以将多个变量合并到一个对象中,然后使用
AtomicReference
来保证这个对象引用的原子性。
6、CAS 的三大经典问题
尽管 CAS 非常高效,但它也存在一些著名的缺陷:
1. ABA 问题
- 描述:一个线程读取内存值为 A,随后另一个线程将其修改为 B,然后又修改回 A。此时,第一个线程执行 CAS 时,会发现值仍然是 A,于是误认为它没有被修改过,CAS 操作成功。这在某些依赖于值的历史状态的场景下会出问题(例如链表操作)。
- 解决方案 :使用版本号 或标记 。Java 中提供了
AtomicStampedReference
(维护一个版本号戳)和AtomicMarkableReference
(维护一个布尔标记)来解决这个问题。
2. 循环时间长开销大
- 描述 :如果 CAS 操作长时间不成功(例如,竞争非常激烈),
for
循环会不停地重试,会占用大量的 CPU 资源。 - 解决方案:对于高竞争的场景,CAS 可能不是最佳选择。可以使用 LongAdder(JDK8),它采用分段锁的思想来降低竞争;或者退回到使用传统的互斥锁。
3. 只能保证一个共享变量的原子操作
- 描述:一个 CAS 操作只能针对一个共享变量。
- 解决方案 :如果需要同时原子性地更新多个变量,可以将多个变量封装到一个对象中。Java 提供了
AtomicReference
来保证引用类型对象的原子性,从而间接实现多个变量的原子更新。
总结
特性 | 描述 |
---|---|
全称 | Compare And Swap(比较并交换) |
本质 | 一条 CPU 原子指令 |
核心思想 | 我认为 V 的值应该是 A,如果是,则更新为 B;否则,不更新并告知失败。 |
在 Java 中的体现 | AtomicXXX 类(如 AtomicInteger )、Unsafe 类 |
优势 | 高性能(无阻塞)、避免死锁 |
劣势 | ABA 问题、高竞争下 CPU 空耗、只能保证一个变量的原子性 |
核心要点:
CAS 是乐观锁 的一种实现方式。它假设冲突很少发生,所以先大胆地去操作,如果发现冲突(CAS 失败),再重试。这与传统的悲观锁 (如 synchronized
)形成鲜明对比,悲观锁假设冲突经常发生,所以每次操作前都要先加锁,确保独占访问。
理解 CAS 是理解现代高性能并发库(如 java.util.concurrent
包)如何工作的关键。
1️⃣ 参考 1
CAS 是现代计算机科学中一个极其重要的概念,它是构建无锁并发数据结构、实现高效同步机制的基石。
1. 什么是 CAS?
CAS ,全称 Compare and Swap ,是一种原子操作 。它用于在多线程环境下实现同步,而无需使用传统的锁(如 synchronized
或 ReentrantLock
)。
它的核心思想是:"我认为值 V 应该是 A,如果是,那么把它更新为 B;否则,不要修改它,但要告诉我现在实际的值是什么。"
2. CAS 的操作语义
CAS 操作通常包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
其语义可以用以下伪代码来理解:
typescript
boolean CAS(V, A, B) {
if (V == A) { // 只有在当前值符合预期时,才进行更新
V = B;
return true; // 成功
} else {
return false; // 失败
}
}
关键在于,这个比较和交换的动作是一个不可分割的原子操作。 在现代CPU上,这通常是由一条特殊的硬件指令(如 x86 架构上的 CMPXCHG
)来完成的,从而保证了在多个线程同时尝试 CAS 同一个内存位置时,不会出现数据竞争。
3. 一个生动的例子:乐观锁
想象一个多人协同编辑的文档(比如 Google Docs)。
- 悲观锁方式 :你告诉所有人"我要开始编辑了,在我保存之前你们都不能动"。(这对应于
synchronized
,你获取了锁,其他线程被阻塞)。 - 乐观锁方式(CAS方式) :
- 你打开文档,看到内容是 "Hello"(这是你的 预期原值 A)。
- 你开始本地编辑,将 "Hello" 改为 "Hello World!"(这是你的 新值 B)。
- 你点击保存。系统在执行保存操作时,会执行一个 CAS :
- 检查 :当前服务器上的文档内容是否还是 "Hello"?(比较
V == A
) - 如果是 ,说明在你编辑期间没有其他人修改,系统将内容更新为 "Hello World!",保存成功。
- 如果否 ,说明在你编辑期间(比如小王把内容改成了 "Hi"),系统不会覆盖小王的修改,你的保存失败 。你需要做的是:重新拉取最新的文档(获取新的预期值) ,然后基于新内容再次进行编辑和保存(重试)。
- 检查 :当前服务器上的文档内容是否还是 "Hello"?(比较
这个"保存时检查"的过程,就是 CAS 的精髓。它避免了长时间的阻塞,允许多个参与者同时工作,只在最后提交时解决冲突。
4. CAS 在 Java 中的实现
在 Java 中,CAS 操作并不是通过关键字直接暴露给开发者的,而是通过 sun.misc.Unsafe
类中的本地方法(compareAndSwapObject
, compareAndSwapInt
, compareAndSwapLong
)来实现的。
然而,对于普通开发者,更常用的是 java.util.concurrent.atomic
包下的原子类,它们封装了这些底层的 CAS 操作。
以 AtomicInteger
为例:
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
public static void main(String[] args) {
AtomicInteger atomicInt = new AtomicInteger(0); // 初始值为 0
// 模拟一个典型的 CAS 操作:如果当前值是 0,就把它设置为 1
boolean success1 = atomicInt.compareAndSet(0, 1); // CAS(V=atomicInt, A=0, B=1)
System.out.println("第一次 CAS: " + success1 + ", 当前值: " + atomicInt.get()); // 输出: true, 1
// 再次尝试:当前值已经是 1 了,但我们仍预期它是 0
boolean success2 = atomicInt.compareAndSet(0, 2); // CAS(V=atomicInt, A=0, B=2)
System.out.println("第二次 CAS: " + success2 + ", 当前值: " + atomicInt.get()); // 输出: false, 1
}
}
另一个核心方法:getAndIncrement()
我们经常使用的 i++
操作不是原子的。但 AtomicInteger
的 getAndIncrement()
是原子的,它的内部就是通过 CAS 实现的,通常被称为 自旋。
java
public final int getAndIncrement() {
// 这是一个典型的自旋循环 CAS 模式
for (;;) { // 循环,直到成功为止
int current = get(); // 获取当前值,作为预期原值 A
int next = current + 1; // 计算新值 B
if (compareAndSet(current, next)) { // 尝试 CAS
return current; // 如果成功,返回旧值
}
// 如果失败,循环重试
}
}
5. CAS 的优缺点
优点:
- 高性能:在竞争不激烈的情况下,它避免了线程挂起和上下文切换的开销,比传统的锁拥有更好的性能。
- 避免死锁:由于是无锁操作,从根本上避免了死锁问题。
- 非阻塞性:一个线程的失败不会导致其他线程阻塞,它们可以立即重试或进行其他操作。
缺点:
-
ABA 问题:
- 描述:假设一个变量的值原来是 A,被线程 1 读到了。之后,线程 2 将其修改为 B,然后又修改回 A。这时,线程 1 执行 CAS 操作,发现值还是 A,于是认为没有人修改过,CAS 成功。但这可能是一个错误的前提,因为变量的历史发生了变化。
- 例子:你看到冰箱里有一瓶可乐(A),然后你去干活了。你老婆把可乐喝了,把瓶子扔了(B),然后觉得过意不去,又买了一瓶新的同款可乐放回冰箱(A)。你回来一看,可乐还在,以为没人动过。
- 解决方案 :使用版本号 或标记 。Java 中提供了
AtomicStampedReference
和AtomicMarkableReference
来解决 ABA 问题。它们不仅在比较值,还会比较一个版本号(Stamp)。
-
循环时间长开销大:
- 如果 CAS 操作长时间不成功(竞争激烈),
for
循环会一直重试,会占用大量的 CPU 资源。如果线程数非常多,不如使用传统的锁效率高。
- 如果 CAS 操作长时间不成功(竞争激烈),
-
只能保证一个共享变量的原子操作:
- 一个 CAS 操作只能作用于一个共享变量。如果要保证多个变量的原子性,可以尝试将它们合并到一个对象中,然后使用
AtomicReference
来 CAS 这个对象。
- 一个 CAS 操作只能作用于一个共享变量。如果要保证多个变量的原子性,可以尝试将它们合并到一个对象中,然后使用
6. 总结
特性 | CAS(乐观锁) | 传统锁(悲观锁) |
---|---|---|
核心思想 | 先操作,冲突时重试 | 先获取锁,再操作 |
阻塞性 | 非阻塞 | 阻塞 |
性能 | 低竞争下,性能极高 | 高竞争下,可能更稳定 |
缺点 | ABA问题、CPU空转 | 死锁、性能开销大 |
适用场景 | 计数器、栈顶/队头更新等简单操作 | 复杂的临界区操作 |
总而言之,CAS 是一种强大的低级原语,它为实现高效、无锁的并发算法提供了可能。 虽然直接使用它的场景有限,但理解其原理对于使用 java.util.concurrent
包、理解数据库的 MVCC 机制以及设计高性能系统都至关重要。
2️⃣ 参考 2
CAS(Compare and Swap)。这是一个在并发编程中至关重要的核心概念。
1. 什么是 CAS?
CAS ,全称 Compare and Swap(比较并交换),是一种用于实现多线程同步的原子操作。
它的核心思想是:"我认为V的值应该是A,如果是,那么将它更新为B;否则,不要修改它,但要告诉我现在实际的值是多少。"
一个 CAS 操作包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
2. CAS 的工作原理
CAS 的运作流程可以用以下伪代码来理解:
arduino
boolean compareAndSwap(MemoryAddress V, int A, int B) {
if (*V == A) { // 步骤1:比较
*V = B; // 步骤2:交换
return true;
} else {
return false;
}
}
具体步骤:
- 比较 :检查内存位置
V
的当前值是否与预期原值A
相等。 - 交换 :
- 如果相等,处理器会自动将该位置的值更新为新值
B
,并返回true
表示操作成功。 - 如果不相等,说明在此期间有其他线程修改了
V
的值。操作失败,不会进行任何更新,并返回false
。此时,调用者可以获取当前的最新值,并重新尝试操作。
- 如果相等,处理器会自动将该位置的值更新为新值
关键在于,整个"比较-交换"过程是一个单一的、不可分割的硬件原子指令。这意味着在操作过程中,不会被其他线程中断,从而保证了多线程环境下的数据安全。
3. 一个生动的例子
假设一个共享变量 count
的初始值为 5。
线程A 和 线程B 都想将其增加 1。
-
线程A 执行 CAS 操作:
V = &count
,A = 5
,B = 6
CAS(&count, 5, 6)
-> 成功!count
现在为 6。
-
几乎同时,线程B 也试图执行 CAS 操作:
V = &count
,A = 5
,B = 6
(注意:线程B读取到的预期原值还是旧的 5)CAS(&count, 5, 6)
-> 失败!因为count
的当前值 (6) 不等于它的预期原值 (5)。
-
线程B 的操作失败后,它会进行"自旋":
- 它重新读取
count
的最新值 (6)。 - 然后发起新的 CAS 操作:
CAS(&count, 6, 7)
。 - 这次操作成功,
count
被更新为 7。
- 它重新读取
这个过程就像是一个乐观的并发控制策略:我先假定没什么人和我竞争,如果事实证明我错了,那我就再试一次。
4. CAS 在 Java 中的实现
在 Java 中,CAS 操作是通过 sun.misc.Unsafe
类中的本地方法(Native Methods)实现的。这些方法最终会调用处理器底层的原子指令。
对于我们普通开发者来说,最直接的接触点是 java.util.concurrent.atomic
包下的原子类,例如:
AtomicInteger
AtomicLong
AtomicReference
AtomicStampedReference
(用于解决 ABA 问题)
示例:使用 AtomicInteger
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet(); // 底层使用 CAS 实现
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet(); // 底层使用 CAS 实现
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + count.get()); // 总是 20000
}
}
incrementAndGet()
的内部实现就是一个典型的 CAS 自旋:
java
public final int incrementAndGet() {
for (;;) { // 自旋循环
int current = get(); // 获取当前值
int next = current + 1; // 计算新值
if (compareAndSet(current, next)) { // 尝试 CAS
return next; // 成功则返回
}
// 失败则立即重试
}
}
5. CAS 的优缺点
优点:
- 高性能 :相比传统的重量级锁(如
synchronized
),CAS 避免了线程的阻塞和唤醒,这是一种在用户态完成的轻量级操作,减少了内核态和用户态切换的开销。在竞争不激烈的情况下,性能远高于锁。 - 避免死锁:由于线程不会阻塞,从根本上避免了死锁问题。
缺点:
-
ABA 问题:
- 描述:一个变量原来是 A,被线程1改为 B,然后又被人改回了 A。这时,线程2进行 CAS 操作时,会发现当前值确实是 A,于是误以为它没有被修改过,从而操作成功。这对于只关心结果值的操作可能没问题,但如果过程很重要(例如链表的头节点),就会出问题。
- 解决方案 :使用版本号或时间戳。Java 提供了
AtomicStampedReference
,它不仅在更新值时比较引用,还会比较一个int
类型的版本戳(Stamp)。
-
循环时间长开销大:
- 如果 CAS 操作长时间不成功(竞争激烈),会导致线程长时间自旋,白白消耗 CPU 资源。如果线程数非常多,对 CPU 的消耗会很大。
-
只能保证一个共享变量的原子操作:
- 当对一个共享变量执行操作时,可以使用循环 CAS 的方式来保证原子操作。但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性。
- 解决方案 :
- 可以用锁。
- 可以将多个变量合并到一个对象里,然后使用
AtomicReference
来保证这个复合对象的原子性。
总结
特性 | 描述 |
---|---|
核心思想 | 无锁编程、乐观锁。先尝试更新,如果失败就重试。 |
操作原子性 | 由硬件(CPU)指令保证,是现代多核处理器并发的基础。 |
在 Java 中 | 通过 Unsafe 类和 java.util.concurrent.atomic 包下的原子类暴露给开发者。 |
主要优势 | 高性能,在低到中度竞争环境下表现优异。 |
主要挑战 | ABA 问题、自旋导致的 CPU 开销、只能保证单个变量的原子性。 |
CAS 是现代并发包(如 Java 的 java.util.concurrent
)的基石,理解了 CAS,就理解了为什么在高并发场景下,无锁算法和数据结构能够如此高效。
3️⃣ 参考 3
CAS (Compare and Swap) 详解
什么是 CAS
CAS (Compare and Swap,比较并交换)是一种无锁的原子操作,用于实现多线程环境下的同步机制。它通过硬件指令保证操作的原子性,是现代并发编程中的重要基础。
CAS 工作原理
基本操作流程
diff
CAS(V, E, N)
- V: 要更新的内存位置
- E: 期望的旧值
- N: 要设置的新值
操作步骤:
- 比较内存位置 V 的值是否与期望值 E 相等
- 如果相等,则将内存位置 V 的值更新为新值 N
- 如果不相等,说明有其他线程修改了该值,操作失败
伪代码实现
cpp
bool CAS(int* ptr, int expected, int new_value) {
if (*ptr == expected) {
*ptr = new_value;
return true;
}
return false;
}
CAS 的特点
优点
- 无锁编程:避免线程阻塞,提高性能
- 原子性:硬件保证操作的原子性
- 高并发:多个线程可以同时竞争,不会阻塞
缺点
- ABA 问题:值从 A 变为 B 又变回 A,CAS 会误认为没有变化
- 循环时间长开销大:如果竞争激烈,会导致长时间自旋
- 只能保证一个变量的原子操作:对于多个变量的原子操作需要额外处理
Java 中的 CAS 实现
Atomic 类使用示例
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int oldValue, newValue;
do {
oldValue = count.get(); // 获取当前值
newValue = oldValue + 1; // 计算新值
} while (!count.compareAndSet(oldValue, newValue)); // CAS 操作
}
public int getCount() {
return count.get();
}
}
简化写法
java
public class SimpleCASExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 内部使用 CAS 实现
}
}
解决 ABA 问题
使用版本号/标记
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAExample {
private AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(0, 0);
public void updateValue(int expectedValue, int newValue) {
int[] stampHolder = new int[1];
int currentValue = atomicRef.get(stampHolder);
int currentStamp = stampHolder[0];
// 比较值和版本号
if (currentValue == expectedValue) {
atomicRef.compareAndSet(expectedValue, newValue,
currentStamp, currentStamp + 1);
}
}
}
CAS 底层实现
硬件支持
- x86 :
CMPXCHG
指令 - ARM :
LDREX/STREX
指令对 - 这些指令在硬件层面保证了比较和交换的原子性
Java 中的 Unsafe 类
java
// Java 底层通过 Unsafe 类实现 CAS
public final class AtomicInteger extends Number {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
实际应用场景
1. 计数器
java
public class Counter {
private AtomicLong count = new AtomicLong(0);
public long increment() {
return count.incrementAndGet();
}
}
2. 非阻塞栈
java
public class ConcurrentStack<E> {
private AtomicReference<Node<E>> top = new AtomicReference<>();
public void push(E item) {
Node<E> newHead = new Node<>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null) return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
private static class Node<E> {
final E item;
Node<E> next;
Node(E item) {
this.item = item;
}
}
}
3. 乐观锁
java
public class OptimisticLockExample {
private AtomicInteger version = new AtomicInteger(0);
private String data;
public boolean updateData(String newData, int expectedVersion) {
// 检查版本号是否变化
if (version.compareAndSet(expectedVersion, expectedVersion + 1)) {
this.data = newData;
return true;
}
return false; // 版本冲突,更新失败
}
}
性能考虑
适用场景
- 低竞争环境:CAS 性能优于锁
- 短时操作:CAS 操作应该快速完成
- 读多写少:读操作不需要同步
不适用场景
- 高竞争环境:大量线程竞争同一资源会导致频繁自旋
- 长时间操作:CAS 循环会占用 CPU 资源
- 复杂操作:需要保护多个变量的操作
总结
CAS 是现代并发编程的核心技术之一,它提供了高效的无锁同步机制。虽然存在 ABA 问题和高竞争下的性能问题,但在合适的场景下,CAS 能够显著提升程序性能。理解 CAS 的原理和适用场景对于编写高性能的并发程序至关重要。
4️⃣ 参考 4
我们来对 CAS(Compare and Swap) 进行一次详细的解析。这是一个在并发编程中至关重要的核心概念。
一、什么是 CAS?
CAS ,全称 Compare and Swap ,中文可译为"比较并交换"。它是一种原子操作,用于在多线程环境下实现无锁(lock-free)的线程安全。
它的核心思想是:"我认为V的值应该是A,如果是,那么将V的值更新为B;否则,不修改并告诉我当前V的实际值是多少。"
二、CAS 的工作原理
一个 CAS 操作包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
其执行流程可以用以下伪代码来理解:
kotlin
function CAS(V, A, B) is
// 1. 读取内存位置V的当前值
temp = *V
// 2. 比较当前值是否与预期原值A相等
if temp == A then
// 3. 如果相等,说明内存值未被其他线程修改,则将新值B写入V
*V = B
// 4. 返回成功
return true
else
// 5. 如果不相等,说明内存值已经被其他线程修改,则操作失败。返回当前的实际值(或失败状态)
return false (and optionally return temp)
这个过程是原子性的,意味着在比较和交换之间,不会有其他线程插入来修改内存位置V的值。
一个生动的比喻:
假设一个共享储物柜(内存位置 V),你认为里面放的是你的书(预期原值 A),你想用你的笔记本电脑(新值 B)换回这本书。
- 你打开储物柜(读取当前值)。
- 你看到里面放的确实是你想找的书(当前值 == A)。
- 你立刻把书拿出来,并把笔记本电脑放进去(执行交换,V = B)。操作成功!
但是,如果在你看储物柜(步骤1)和准备交换(步骤3)的瞬间,你的朋友过来把他的书拿走了,并放了一个水杯进去。那么:
- 你打开储物柜(读取当前值)。
- 你发现里面是一个水杯(当前值 != A)。
- 你意识到情况有变,交换失败。你现在知道了储物柜里的最新东西是水杯(新的预期值)。
三、CAS 的底层硬件实现
CAS 之所以能成为原子操作,并不是通过软件锁实现的,而是依赖于底层硬件(主要是CPU)提供的指令。
- x86 架构 :
CMPXCHG
指令(Compare and Exchange)。 - ARM 架构 :
LDREX
和STREX
指令对,用于实现类似的加载-链接/条件存储操作。
现代的多核处理器都在硬件层面直接支持这些原子操作指令,确保了它们在多核环境下的原子性。
四、CAS 在 Java 中的应用
在 Java 中,CAS 操作是由 sun.misc.Unsafe
类中的本地方法(Native Method)提供的,例如 compareAndSwapInt
, compareAndSwapLong
等。
然而,对于普通开发者来说,我们通常不会直接使用 Unsafe
类。而是通过 java.util.concurrent.atomic
包下的原子类来间接使用 CAS。
最典型的例子:AtomicInteger
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 这行代码底层就使用了 CAS
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + count.get()); // 总是 20000
}
}
incrementAndGet()
的内部实现,就是一个典型的 CAS + 自旋 循环:
java
public final int incrementAndGet() {
// 自旋循环
for (;;) {
int current = get(); // 获取当前值,作为预期原值 A
int next = current + 1; // 计算新值 B
// 调用 CAS:如果 count 的当前值还是 current,就把它更新为 next
if (compareAndSet(current, next)) {
return next; // 成功则返回新值
}
// 失败则循环重试,重新读取最新的 current 值
}
}
五、CAS 的三大经典问题
尽管 CAS 非常高效,但它也存在一些著名的缺陷:
- ABA 问题
- 描述:一个线程读取内存值为 A,随后另一个线程将其修改为 B,然后又修改回 A。此时,第一个线程执行 CAS 时,会发现值仍然是 A,于是误认为它没有被修改过,CAS 操作成功。这在某些依赖于值的历史状态的场景下会出问题(例如链表操作)。
- 解决方案 :使用版本号 或标记 。Java 中提供了
AtomicStampedReference
(维护一个版本号戳)和AtomicMarkableReference
(维护一个布尔标记)来解决这个问题。
- 循环时间长开销大
- 描述 :如果 CAS 操作长时间不成功(例如,竞争非常激烈),
for
循环会不停地重试,会占用大量的 CPU 资源。 - 解决方案:对于高竞争的场景,CAS 可能不是最佳选择。可以使用 LongAdder(JDK8),它采用分段锁的思想来降低竞争;或者退回到使用传统的互斥锁。
- 只能保证一个共享变量的原子操作
- 描述:一个 CAS 操作只能针对一个共享变量。
- 解决方案 :如果需要同时原子性地更新多个变量,可以将多个变量封装到一个对象中。Java 提供了
AtomicReference
来保证引用类型对象的原子性,从而间接实现多个变量的原子更新。
六、CAS 的优缺点总结
优点:
- 高性能 :在多数低至中度竞争的情况下,它避免了线程挂起和上下文切换的开销,性能远高于使用锁(如
synchronized
)。 - 无锁(Lock-Free):它是构建非阻塞算法的基础。
缺点:
- ABA 问题:如上所述,需要额外机制来解决。
- 自旋开销:在高竞争环境下,CPU 空转严重。
- 功能局限:只能保证一个变量的原子性。
七、总结
CAS 是现代并发编程的基石之一。它通过利用硬件的原子指令,提供了一种高效的无锁同步机制。理解 CAS 是理解 java.util.concurrent
包中众多高效工具类(如 ConcurrentHashMap
, AQS
等)的关键。
它并非万能的银弹,开发者需要根据具体的竞争强度和业务场景,在 CAS 、锁 和 其他并发工具 之间做出权衡。
5️⃣ 参考 5
CAS (Compare and Swap) 详解
什么是 CAS?
CAS(Compare and Swap,比较并交换) 是一种无锁的原子操作,用于在多线程环境下实现同步而不使用传统的锁机制。它是现代并发编程的基石之一。
CAS 操作原理
CAS 操作包含三个基本操作数:
- 内存位置(V)
- 期望的原值(A)
- 新值(B)
操作过程:
css
如果内存位置 V 的值等于期望值 A,则将该位置的值更新为新值 B
否则,不进行任何操作
CAS 伪代码
cpp
bool CAS(int* ptr, int expected, int new_value) {
if (*ptr == expected) {
*ptr = new_value;
return true;
}
return false;
}
CAS 的特点
原子性 CAS 操作是硬件级别的原子操作,不会被线程调度机制打断。
无锁编程 CAS 是实现无锁(lock-free)数据结构的基础。
乐观锁机制 采用"先尝试,失败重试"的策略,而不是悲观地获取锁。
Java 中的 CAS 实现
Atomic 类使用 CAS
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
oldValue = count.get(); // 读取当前值
newValue = oldValue + 1; // 计算新值
} while (!count.compareAndSet(oldValue, newValue)); // CAS 操作
}
public int getCount() {
return count.get();
}
}
更简洁的写法
java
public void incrementSimple() {
count.incrementAndGet(); // 内部使用 CAS
}
CAS 的典型应用
1. 计数器
java
AtomicInteger counter = new AtomicInteger(0);
// 线程安全的自增
counter.incrementAndGet();
// 线程安全的加法
counter.addAndGet(5);
2. 标志位控制
java
AtomicBoolean flag = new AtomicBoolean(false);
// 只有一个线程能成功设置为 true
if (flag.compareAndSet(false, true)) {
// 执行初始化操作
}
3. 引用类型原子更新
java
AtomicReference<String> reference = new AtomicReference<>("initial");
// 原子更新引用
reference.compareAndSet("initial", "updated");
CAS 的优缺点
优点
- 高性能:避免线程阻塞和上下文切换
- 无死锁:不会产生死锁问题
- 可扩展性:在高并发场景下性能更好
缺点
- ABA 问题:值从 A 变为 B 又变回 A,CAS 会误认为没有变化
- 循环时间长开销大:竞争激烈时可能长时间自旋
- 只能保证一个变量的原子性:不能同时保证多个变量的原子性
解决 ABA 问题
版本号机制
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
private AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(0, 0);
public void update(int expectedValue, int newValue) {
int[] stampHolder = new int[1];
int currentValue = atomicRef.get(stampHolder);
int currentStamp = stampHolder[0];
if (currentValue == expectedValue) {
atomicRef.compareAndSet(currentValue, newValue,
currentStamp, currentStamp + 1);
}
}
}
CAS 底层实现
CPU 硬件支持
- x86:
CMPXCHG
指令 - ARM:
LDREX/STREX
指令对
Java 中的实现
java
// Unsafe 类提供 CAS 操作
public final class Unsafe {
public final native boolean compareAndSwapObject(
Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt(
Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(
Object o, long offset, long expected, long x);
}
实际应用示例
无锁栈实现
java
import java.util.concurrent.atomic.AtomicReference;
public class LockFreeStack<T> {
private static class Node<T> {
final T value;
Node<T> next;
Node(T value) {
this.value = value;
}
}
private AtomicReference<Node<T>> top = new AtomicReference<>();
public void push(T value) {
Node<T> newHead = new Node<>(value);
Node<T> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public T pop() {
Node<T> oldHead;
Node<T> newHead;
do {
oldHead = top.get();
if (oldHead == null) {
return null;
}
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.value;
}
}
性能考虑
适用场景
- 低到中等竞争程度
- 操作简单快速
- 需要高并发性能
不适用场景
- 高竞争环境(可能导致大量重试)
- 复杂操作(CAS 不适合复杂业务逻辑)
- 需要多个变量原子更新的场景
CAS 是现代并发编程的重要工具,理解其原理和适用场景对于编写高性能的并发程序至关重要。
6️⃣ 参考 6
CAS (Compare and Swap) 详解
什么是CAS
CAS(Compare and Swap,比较并交换)是一种无锁(lock-free)的并发控制机制,用于在多线程环境中实现原子操作。它是现代并发编程中重要的同步原语之一。
CAS的基本原理
CAS操作包含三个基本操作数:
- 内存位置(V):需要读写的内存位置
- 预期原值(A):预期内存位置中的原始值
- 新值(B):要写入的新值
CAS的语义是:只有当内存位置V的值等于预期原值A时,才会将V的值更新为新值B,否则不执行任何操作。无论是否更新成功,都会返回当前V的实际值。
CAS的工作流程
- 读取内存位置V的当前值
- 比较V的当前值与预期值A
- 如果相等,将V的值更新为B
- 如果不相等,不执行更新
- 返回V的当前值(无论是否更新成功)
CAS的伪代码表示
arduino
function compareAndSwap(V, A, B):
if V == A:
V = B
return true
else:
return false
CAS的优点
- 无锁性:避免了传统锁带来的线程阻塞和上下文切换开销
- 高性能:在低竞争场景下性能优于基于锁的同步
- 可伸缩性:随着CPU核心数的增加,性能不会显著下降
- 避免死锁:由于不使用锁,自然不会发生死锁
CAS的缺点
- ABA问题:值从A变为B又变回A,CAS会误认为没有变化
- 自旋开销:高竞争场景下,线程会不断重试CAS操作,消耗CPU资源
- 只能保证一个共享变量的原子性:对于多个共享变量的操作,需要额外的同步机制
ABA问题及解决方案
ABA问题示例
- 线程1读取值A
- 线程2将值从A改为B,然后又改回A
- 线程1执行CAS时,发现值仍是A,认为没有被修改过
解决方案
- 版本号/时间戳 :为共享变量添加版本号,每次修改都递增版本号
- Java中的
AtomicStampedReference
- Java中的
- 唯一标识 :使用不可重复的唯一标识代替简单值
- Java中的
AtomicMarkableReference
- Java中的
Java中的CAS实现
Java提供了java.util.concurrent.atomic
包来支持CAS操作:
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
oldValue = counter.get();
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}
public int get() {
return counter.get();
}
}
实际应用场景
- 计数器实现:如上述AtomicInteger示例
- 无锁数据结构:无锁队列、无锁栈等
- 并发集合:ConcurrentHashMap等
- 线程安全对象:如AtomicReference等
与锁的比较
特性 | CAS | 传统锁 |
---|---|---|
性能 | 低竞争时高,高竞争时可能下降 | 高竞争时性能下降明显 |
实现复杂度 | 较高 | 较低 |
死锁风险 | 无 | 有 |
适用场景 | 低竞争、短临界区 | 高竞争、长临界区 |
现代CPU对CAS的支持
大多数现代CPU都提供了CAS指令:
- x86:
CMPXCHG
(Compare and Exchange) - ARM:
LDREX
/STREX
指令对 - PowerPC:
lwarx
/stwcx
指令对
这些硬件指令保证了CAS操作的原子性。
总结
CAS是一种强大的无锁同步机制,适用于低竞争场景下的并发编程。它通过硬件指令保证原子性,避免了传统锁的开销,但需要开发者注意ABA问题和自旋开销等问题。在现代并发编程中,CAS是构建高性能、可伸缩并发应用的重要工具。
7️⃣ 参考 7
CAS(Compare and Swap)详细解释
- 基本定义与原理 CAS(比较并交换)是一种无锁并发控制技术,用于实现多线程环境下的原子操作。其核心包含三个操作数:
- 内存位置(V):要修改的共享变量地址
- 预期原值(A):线程读取该内存位置时的旧值
- 更新值(B):希望写入的新值
操作逻辑:
plaintext
if V == A then
V = B // 更新成功
else
重试或放弃 // 更新失败
该操作由单条CPU指令 (如x86的CMPXCHG
)原子性完成,确保比较和更新过程不可分割。
-
工作流程 以两个线程竞争修改共享变量
V=10
为例: -
线程1和线程2同时读取
V=10
-
线程1执行
CAS(V, 10, 20)
:- 比较发现
V==10
(匹配)→ 更新V=20
成功
- 比较发现
-
线程2执行
CAS(V, 10, 30)
:- 此时
V=20≠10
(不匹配)→ 更新失败 → 重新读取V=20
并自旋重试
- 此时
-
核心特点
优势 | 劣势 |
---|---|
无锁算法:避免线程阻塞和上下文切换开销 | ABA问题:值从A→B→A变化时,CAS误判未修改 |
乐观锁:假设冲突概率低,直接尝试更新 | 自旋消耗CPU:竞争激烈时循环重试浪费资源 |
原子性:硬件级指令保证操作不可分割 | 仅适用简单操作:复杂逻辑需配合其他机制 |
- 典型应用场景
-
原子类实现 :如Java的
AtomicInteger
,通过CAS实现incrementAndGet()
java// 伪代码实现 do { int oldValue = get(); // 读取当前值 int newValue = oldValue + 1; } while (!CAS(oldValue, newValue)); // 自旋直到成功
-
无锁数据结构:并发队列(如Disruptor RingBuffer)、并发栈
-
同步机制基础:实现自旋锁、信号量等
- ABA问题解决方案 通过添加版本号/时间戳扩展CAS操作:
-
原始值:
A
-
扩展结构:
(A, Version=1)
-
更新时同时校验值和版本号:
plaintextif (V.value == A && V.version == 1) then update to (B, 2)
例如Java的
AtomicStampedReference
。
相关问题
- CAS操作在哪些场景下性能优于传统锁机制?
- 如何通过Java代码实现一个基于CAS的自旋锁?
- ABA问题在实际系统中可能引发哪些具体风险?请举例说明
- 对比CAS与volatile关键字在并发控制中的异同
- 现代CPU如何通过硬件指令(如LL/SC)实现CAS的原子性?
8️⃣ 参考 8
CAS(Compare and Swap,比较并交换)是一种无锁同步机制,核心是通过硬件指令保证操作的原子性,实现多线程下对共享变量的安全修改,无需依赖锁。
一、CAS的核心工作原理 CAS操作依赖三个关键参数,通过"比较-交换"的原子步骤完成变量更新,具体流程如下:
- 确定三个核心参数 :
- 内存地址V:要修改的共享变量在内存中的地址。
- 预期值A:线程读取到的变量当前值(修改前的"旧值")。
- 新值B:线程希望将变量更新为的目标值。
- 执行原子操作 :
- 硬件层面(如CPU的
cmpxchg
指令)原子性地比较"内存地址V中的实际值"与"预期值A"。 - 若两者相等:说明变量未被其他线程修改,将"新值B"写入内存地址V,操作成功。
- 若两者不相等:说明变量已被其他线程修改,放弃写入,操作失败(线程通常会重试或直接返回)。
- 硬件层面(如CPU的
二、CAS的关键特性(优缺点) CAS通过无锁设计解决了传统锁的开销问题,但也存在固有局限,具体如下:
- 优点
- 无锁高效:无需加锁/解锁,避免了线程阻塞、上下文切换的开销,在低并发场景下性能优于锁机制。
- 原子性保障 :依赖CPU硬件指令(如x86的
cmpxchg
、ARM的ldrex/strex
),天然保证"比较-交换"步骤的原子性,无需额外同步。 - 非阻塞:失败的线程不会阻塞,可选择重试或直接退出,不会影响其他线程的执行。
- 缺点
- ABA问题:变量值从A被改为B,又改回A,CAS会误认为"变量未被修改"而成功更新,可能导致逻辑错误(如链表节点复用场景)。
- 自旋消耗 :若高并发下变量频繁被修改,CAS会反复重试,持续占用CPU资源(如Java中
AtomicInteger
的getAndIncrement
)。 - 只能保证单个变量原子性 :CAS仅能对单个共享变量的更新做原子性保障,无法直接处理多个变量的组合原子操作(需结合
volatile
或其他机制)。
三、ABA问题的解决方案 针对ABA问题,主流方案是为变量增加"版本号"或"时间戳",让变量的"值+版本号"共同作为判断依据,具体实现如下:
- 核心思路:修改共享变量时,不仅更新值,还同步递增版本号;CAS比较时,同时比较"预期值"和"预期版本号"。
- 实际案例 :Java中的
AtomicStampedReference
,通过"对象引用+整数戳"的组合,确保即使值相同,版本号不同也会判定为修改,避免ABA问题。
四、CAS的实际应用场景 CAS是很多并发工具的底层实现,常见应用包括:
- 原子类 :Java的
java.util.concurrent.atomic
包(如AtomicInteger
、AtomicLong
),通过CAS实现变量的原子增减、更新。 - 并发容器 :
ConcurrentLinkedQueue
(无锁队列),通过CAS实现节点的入队、出队操作,避免锁竞争。 - 锁优化:Synchronized锁的"轻量级锁"阶段,通过CAS尝试将对象头的"Mark Word"设置为线程ID,避免升级为重量级锁。
要不要我帮你整理一份CAS核心知识点脑图?把工作原理、优缺点、解决方案和应用场景按逻辑串联,方便你快速梳理和记忆。
9️⃣ 参考 9
CAS(Compare and Swap,比较并交换)是一种无锁并发编程的核心技术,广泛应用于多线程环境下的原子操作。它通过硬件指令保证操作的原子性,避免了传统锁机制带来的性能开销和死锁风险。
一、CAS 的基本概念
✅ 定义: CAS 是一种原子操作 ,用于实现多线程环境下的无锁更新。
✅ 操作逻辑: CAS 操作包含三个参数:
- 内存地址 V:要更新的变量
- 预期值 A:当前线程认为 V 的值应该是 A
- 新值 B:希望将 V 更新为 B
操作语义:
text
if V == A:
V = B
return true
else:
return false
只有当 V 的当前值等于预期值 A 时,才会将 V 更新为 B,否则不做任何操作。
二、CAS 的底层支持
CAS 操作依赖于CPU 提供的原子指令,例如:
平台 | 指令 |
---|---|
x86 | CMPXCHG |
ARM | LDREX/STREX |
这些指令保证了 CAS 操作的原子性,即使在多核环境下也不会出现竞态条件。
三、CAS 的应用场景
✅ 原子类(Java 中的 AtomicInteger , AtomicReference 等)
java
AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(0, 1); // 如果当前值是0,则更新为1
✅ 无锁数据结构
- 无锁队列(Lock-free Queue)
- 无锁栈(Lock-free Stack)
- 无锁哈希表(ConcurrentHashMap 的某些实现)
✅ 自旋锁(Spinlock) 使用 CAS 实现简单的自旋锁:
c
int lock = 0;
void acquire() {
while (!CAS(&lock, 0, 1)); // 自旋等待
}
void release() {
lock = 0;
}
四、CAS 的优点
优点 | 说明 |
---|---|
✅ 无锁 | 避免死锁、线程阻塞 |
✅ 高性能 | 无需内核调度,用户态完成 |
✅ 可扩展 | 适合高并发场景 |
五、CAS 的缺点与问题
❌ 1. ABA 问题 问题描述 :
线程 T1 读取值为 A,被挂起;线程 T2 将 A → B → A;T1 恢复后认为值没变,CAS 成功,但实际上中间状态已变。
解决方案:
- 版本号机制 (如 Java 的
AtomicStampedReference
) - 指针标记(低 bit 标记版本)
❌ 2. 自旋开销 CAS 失败时会重试,可能导致忙等待(busy waiting),浪费 CPU。
❌ 3. 仅保证单个变量原子性 CAS 只能操作一个变量,无法原子地更新多个变量。
六、CAS 与锁的对比
特性 | CAS | 锁 |
---|---|---|
是否阻塞 | ❌ 不阻塞 | ✅ 可能阻塞 |
死锁风险 | ❌ 无 | ✅ 有 |
性能 | 高并发下更好 | 低并发下稳定 |
复杂度 | 高(ABA、重试) | 低 |
适用范围 | 原子变量、无锁结构 | 任意临界区 |
七、总结一句话
CAS 是一种乐观并发策略,它假设冲突很少发生,失败就重试,避免锁的开销,但需要处理 ABA 和自旋等问题。
✅ 拓展阅读(可选)
- Java 源码:
sun.misc.Unsafe.compareAndSwapInt()
- C++11:
std::atomic<T>::compare_exchange_weak()
- Linux 内核:
cmpxchg()
宏实现
如果你有具体语言(如 Java、C++、Rust)或场景(如队列、计数器)想深入,我可以给你写代码示例。
🔟 参考 10
CAS(Compare and Swap)详细解释
1. 基本定义与原理 CAS(比较并交换)是一种无锁并发控制技术,用于实现多线程环境下的原子操作。其核心包含三个操作数:
- 内存位置(V):要修改的共享变量地址
- 预期原值(A):线程读取该内存位置时的旧值
- 更新值(B):希望写入的新值
操作逻辑:
plaintext
if V == A then
V = B // 更新成功
else
重试或放弃 // 更新失败
该操作由单条CPU指令 (如x86的CMPXCHG
)原子性完成,确保比较和更新过程不可分割。
2. 工作流程 以两个线程竞争修改共享变量V=10
为例:
- 线程1和线程2同时读取
V=10
- 线程1执行
CAS(V, 10, 20)
:- 比较发现
V==10
(匹配)→ 更新V=20
成功
- 比较发现
- 线程2执行
CAS(V, 10, 30)
:- 此时
V=20≠10
(不匹配)→ 更新失败 → 重新读取V=20
并自旋重试
- 此时
3. 核心特点
优势 | 劣势 |
---|---|
无锁算法:避免线程阻塞和上下文切换开销 | ABA问题:值从A→B→A变化时,CAS误判未修改 |
乐观锁:假设冲突概率低,直接尝试更新 | 自旋消耗CPU:竞争激烈时循环重试浪费资源 |
原子性:硬件级指令保证操作不可分割 | 仅适用简单操作:复杂逻辑需配合其他机制 |
4. 典型应用场景
-
原子类实现 :如Java的
AtomicInteger
,通过CAS实现incrementAndGet()
java// 伪代码实现 do { int oldValue = get(); // 读取当前值 int newValue = oldValue + 1; } while (!CAS(oldValue, newValue)); // 自旋直到成功
-
无锁数据结构:并发队列(如Disruptor RingBuffer)、并发栈
-
同步机制基础:实现自旋锁、信号量等
5. ABA问题解决方案 通过添加版本号/时间戳扩展CAS操作:
-
原始值:
A
-
扩展结构:
(A, Version=1)
-
更新时同时校验值和版本号:
plaintextif (V.value == A && V.version == 1) then update to (B, 2)
例如Java的
AtomicStampedReference
。
相关问题
- CAS操作在哪些场景下性能优于传统锁机制?
- 如何通过Java代码实现一个基于CAS的自旋锁?
- ABA问题在实际系统中可能引发哪些具体风险?请举例说明
- 对比CAS与volatile关键字在并发控制中的异同
- 现代CPU如何通过硬件指令(如LL/SC)实现CAS的原子性?
🤔 参考 11
CAS(Compare-And-Swap,比较并交换)是一种非常重要的无锁(Lock-Free)并发编程技术,它通过硬件原子指令实现多线程环境下的安全数据操作,避免了传统锁机制的开销。
下面是一个核心概览表,帮你快速了解CAS:
方面 | 核心要点 |
---|---|
核心思想 | 我认为内存位置 V 的值应该是 A。如果是,我就把它更新为 B;如果不是,我就不更新(通常会重试)。整个过程是原子的。 |
操作数 | 内存位置 (V)、预期原值 (A)、新值 (B) |
底层支撑 | CPU硬件指令(如x86的 CMPXCHG ) |
本质 | 一种乐观锁实现 |
关键优点 | 无锁,减少线程阻塞和上下文切换,在高并发低竞争场景下性能优异 |
经典问题 | ABA问题 (可通过AtomicStampedReference 带版本号解决) |
常见应用 | AtomicInteger 等原子类、自旋锁、无锁队列、乐观锁机制 |
🔧 工作原理与步骤
CAS操作是一个原子指令,其执行流程如下:
- 读取(Read) :读取内存位置
V
的当前值,作为预期值A
。 - 比较(Compare) :比较内存位置
V
的当前值是否仍等于预期值A
。- 如果相等,说明从读取到现在,没有其他线程修改过此值。
- 如果不相等,说明值已被其他线程修改,操作失败。
- 交换(Swap) :只有在比较成功时,才会将内存位置
V
的值更新为新值B
。 - 结果返回 :操作会返回是否成功的状态。失败后,常见的策略是自旋(Spin),即循环重试直到成功。
其基本流程可概括为:
plaintext
读取V的值 -> 比较V的值是否等于A -> [等于] 则将V的值更新为B -> 返回成功
-> [不等于] 返回失败(通常伴随重试)
⚙️ 在Java中的实现
Java主要通过 sun.misc.Unsafe
类(底层调用Native方法)和 java.util.concurrent.atomic
包下的原子类 (如 AtomicInteger
, AtomicLong
, AtomicReference
)来提供CAS操作。
例如 AtomicInteger
的 incrementAndGet()
方法底层实现:
java
public final int incrementAndGet() {
int current;
do {
current = get(); // 获取当前值
// 循环尝试CAS,直到成功为止
} while (!compareAndSet(current, current + 1));
return current + 1;
}
AtomicInteger
内部使用了一个 volatile
修饰的 value
字段来保证可见性,并通过 Unsafe.compareAndSwapInt
方法实现CAS操作,该方法参数包括对象实例、字段内存偏移量、预期值和新值。
💡 CAS的优劣势
- 优点 :
- 高性能 :避免了重量级锁带来的线程阻塞、上下文切换和内核态切换的开销,在低竞争度的并发场景下性能显著优于锁。
- 无死锁:由于线程不会阻塞,从根本上避免了死锁问题(但可能活锁)。
- 缺点与挑战 :
- ABA问题 :这是CAS的一个典型陷阱。假设变量值原来是A,被另一个线程先改为B后又改回A,那么CAS检查时会误以为它从未被修改过。解决方案是为数据加上版本号或时间戳 (如
AtomicStampedReference
)。 - 自旋开销 :在高竞争环境下,如果线程长时间CAS失败不停重试,会空耗CPU资源。。
- 单变量原子性 :CAS通常只能保证一个共享变量的原子操作。要对多个变量进行原子更新,需使用锁或将多个变量合并到一个对象中,再用
AtomicReference
来引用。
- ABA问题 :这是CAS的一个典型陷阱。假设变量值原来是A,被另一个线程先改为B后又改回A,那么CAS检查时会误以为它从未被修改过。解决方案是为数据加上版本号或时间戳 (如
🎯 主要应用场景
- 原子类 :Java中的
AtomicInteger
,AtomicLong
等原子类,其核心实现依赖于CAS。 - 自旋锁(SpinLock):线程通过循环CAS尝试获取锁,避免立即进入阻塞状态。
- 无锁数据结构 :如无锁队列(
ConcurrentLinkedQueue
)、无锁栈等,利用CAS实现线程安全的插入和删除。 - 乐观锁机制:数据库或缓存系统中的乐观锁,通常基于版本号(Version)和CAS思想实现。
⚠️ 使用注意事项
- 竞争程度 :CAS适用于低到中度线程竞争 的场景。若竞争非常激烈,自旋开销可能很大,此时传统锁可能是更好选择。。
- ABA问题不可忽视 :只要值会发生循环变化,就需使用
AtomicStampedReference
等带版本号的原子类来规避风险。 - 复杂度权衡 :无锁编程的正确性难以保证,设计复杂。除非确有必要,否则优先选择高级并发工具(如
java.util.concurrent
包中的类),而非直接操作CAS。
💎 总结
CAS是理解现代高并发编程的基石。它通过硬件辅助的原子操作,巧妙地避免了锁的诸多开销,在特定场景下能带来巨大的性能提升。
理解CAS的核心在于掌握其乐观并发 的思想:先冒险操作,遇到冲突再重试。同时,务必警惕其著名的 ABA问题。
希望这份详解对你有帮助。
如果你想了解特定并发场景下是选择CAS还是锁更合适,或者想深入了解无锁数据结构的设计,我很乐意继续与你探讨。
🤔 参考 12
CAS(Compare and Swap,比较并交换)是一种无锁并发编程的核心机制,通过硬件指令实现原子操作,确保多线程环境下共享数据的一致性。以下是其核心原理、应用场景及局限性的综合分析:
一、CAS 的核心原理
-
操作逻辑
CAS 包含三个参数:内存地址(V)、预期值(A)、新值(B)。其执行流程为:
- 比较:读取内存地址 V 的当前值,与预期值 A 比较。
- 交换:若相等,则将 V 的值更新为 B;否则不修改,返回当前值。
- 原子性 :整个过程由硬件指令(如 x86 的
cmpxchg
)保证,不可被中断。
-
硬件支持
- CPU 指令 :x86 架构的
cmpxchg
、ARM 的ldrex/strex
等指令直接实现 CAS,通过锁总线或缓存一致性协议(如 MESI)保证原子性。 - Java 实现 :通过
sun.misc.Unsafe
类的compareAndSwapInt
等方法调用底层指令,原子类(如AtomicInteger
)封装了这些操作。
- CPU 指令 :x86 架构的
二、CAS 的优势
-
无锁并发
- 避免传统锁的阻塞和上下文切换开销,适合高并发场景(如计数器、状态标志)。
- 示例:
AtomicInteger.incrementAndGet()
通过自旋 CAS 实现线程安全递增。
-
高性能
- 低冲突时性能远超锁机制(如
synchronized
),减少线程阻塞和调度成本。 - 适用于细粒度操作(如单个变量更新),避免锁的粗粒度限制。
- 低冲突时性能远超锁机制(如
-
避免死锁
- 无锁设计天然规避死锁风险,适用于复杂并发控制场景。
三、CAS 的局限性及解决方案
-
ABA 问题
- 现象:变量值从 A → B → A,CAS 误判为未修改,导致逻辑错误。
- 解决方案 :
- 版本号机制 :如
AtomicStampedReference
,通过版本号跟踪变量状态。 - 标记位 :使用
AtomicMarkableReference
标记变量是否被修改。
- 版本号机制 :如
-
自旋开销
- 问题:高竞争时频繁自旋重试,浪费 CPU 资源。
- 优化 :
- 限制重试次数或结合退避策略(如
Thread.yield()
)。 - 自适应自旋(如 JDK 锁优化中的动态调整)。
- 限制重试次数或结合退避策略(如
-
单一变量限制
- 问题:仅支持单个变量的原子操作,多变量需额外处理。
- 方案 :
- 封装多个变量为对象,通过
AtomicReference
操作整体。 - 复杂场景结合锁机制(如
ConcurrentHashMap
的分段锁)。
- 封装多个变量为对象,通过
四、CAS 的应用场景
-
原子计数器
- 如
AtomicInteger
实现线程安全计数,避免锁的开销。
- 如
-
无锁数据结构
- 无锁队列(
ConcurrentLinkedQueue
)、栈等,通过 CAS 实现并发安全。
- 无锁队列(
-
状态标志更新
- 如
AtomicBoolean
控制线程任务启停,确保状态一致性。
- 如
-
乐观锁机制
- 数据库版本号控制、分布式锁(如 Redis 的
SETNX
)均基于 CAS 思想。
- 数据库版本号控制、分布式锁(如 Redis 的
五、CAS 与锁的对比
特性 | CAS(乐观锁) | 锁(悲观锁) |
---|---|---|
核心思想 | 假设无冲突,失败后重试 | 假设必有冲突,先加锁再操作 |
线程状态 | 无阻塞,自旋等待 | 阻塞等待锁释放 |
性能 | 低冲突时高效,高冲突时性能下降 | 高冲突时稳定,低冲突时开销大 |
适用场景 | 读多写少、操作简单 | 写多读少、临界区复杂 |
ABA 问题 | 存在(需额外解决) | 不存在 |
六、总结 CAS 是无锁并发的基石,通过硬件指令实现原子操作,在低冲突场景下显著提升性能。但其局限性(如 ABA 问题、自旋开销)需结合版本号、对象封装等方案解决。实际开发中,应根据场景选择:低竞争用 CAS,高竞争用锁 ,例如 JDK 的 LongAdder
在高并发下分段累加,平衡性能与准确性。
🤔 参考 13
CAS(Compare and Swap,比较并交换)是并发编程中一种非常重要的无锁(Lock-Free)原子操作,它被广泛应用于实现多线程同步(如实现自旋锁、乐观锁)和构建非阻塞数据结构。下面我将从多个角度为你详细解析它。
一、核心思想与原理
核心思想: "我认为值应该是A,如果是,我就把它改成B;如果不是,说明有别人改过了,那我就不修改了。"
这是一个原子操作,意味着比较和交换这两个步骤在执行时是不可分割的,中间不会被打断。
操作数: CAS 操作通常需要三个参数:
- 内存位置(V):要操作的内存地址(例如,一个共享变量的地址)。
- 期望原值(A) :线程认为该内存位置当前应该具有的值。
- 新值(B) :如果期望值匹配,线程希望将该内存位置更新为的新值。
伪代码逻辑:
cpp
function CAS(memory_address, expected_value, new_value) {
if (*memory_address == expected_value) {
*memory_address = new_value;
return true; // 成功
} else {
return false; // 失败
}
}
(注意:实际的CPU指令将整个if
判断和赋值作为一个不可中断的指令执行)
二、底层实现
CAS 的实现高度依赖于硬件。现代大多数CPU架构(如 x86、ARM)都提供了实现CAS的指令:
- x86:
CMPXCHG
(Compare and Exchange) 指令。 - ARM:
LDREX
和STREX
指令组合。
高级编程语言(如 Java、C++)通过调用这些底层硬件指令来提供CAS操作。
- Java: 在
sun.misc.Unsafe
类中有compareAndSwapInt
,compareAndSwapLong
等方法。但我们通常不直接使用它,而是使用封装好的原子类,如AtomicInteger
,AtomicReference
等。 - C++11: 在 `` 头文件中提供了
std::atomic
及其相关的compare_exchange_strong
和compare_exchange_weak
成员函数。
三、一个简单的Java示例
Java中的 AtomicInteger
是使用CAS的经典例子。
java
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
public static void main(String[] args) {
AtomicInteger atomicInt = new AtomicInteger(0); // 初始值为0
// 线程1:尝试将 0 -> 1
boolean success1 = atomicInt.compareAndSet(0, 1); // CAS操作
System.out.println("CAS(0, 1): " + success1 + ", value: " + atomicInt.get());
// 线程2:尝试将 0 -> 2 (但此时值已经是1了,所以会失败)
boolean success2 = atomicInt.compareAndSet(0, 2); // CAS操作
System.out.println("CAS(0, 2): " + success2 + ", value: " + atomicInt.get());
}
}
输出:
objectivec
CAS(0, 1): true, value: 1
CAS(0, 2): false, value: 1
这个例子清晰地展示了CAS的语义:只有在值符合预期时才会修改成功。
四、优点与应用场景
-
性能优势( vs. 锁 ):
- 减少开销:避免了操作系统层面线程挂起和上下文切换的巨大开销。
- 避免死锁:由于不使用锁,从根本上避免了死锁问题。
- 非阻塞:一个线程的失败不会导致其他线程阻塞,它们可以立即重试或进行其他操作。
-
经典应用场景:
- 原子变量 :
AtomicInteger
,AtomicLong
,AtomicReference
等。 - 非阻塞数据结构:无锁队列(Lock-Free Queue)、无锁栈(Lock-Free Stack)等高性能并发容器。
- 乐观锁:在数据库和并发控制中,CAS是实现乐观锁的核心思想。例如,更新数据前先比较版本号。
- 原子变量 :
五、经典问题与局限性
-
ABA 问题
- 描述:线程1读到内存值V为A,准备执行CAS(A->B)。此时线程2将值从A改为C,然后又改回A。线程1执行CAS时,发现值还是A,于是误以为没有变化,操作成功。但这中间可能已经发生了其他有意义的变化(比如链表结构已变)。
- 解决方案 :使用带版本号的原子引用 (Stamp)。每次修改不仅更新值,还递增一个版本号。CAS时同时比较值和版本号。Java中的
AtomicStampedReference
就是为此而生的。
-
循环时间长开销大
- 描述:如果CAS操作一直失败,线程会不断自旋重试(Spin),如果竞争激烈,会长时间占用CPU资源。
- 解决方案:限制自旋次数,或与其他同步机制结合使用(例如,在Java中,JVM会管理自旋锁的自旋次数)。
-
只能保证一个共享变量的原子操作
- 描述:一个CAS操作只能针对一个变量。对多个共享变量操作时,CAS无法保证其原子性。
- 解决方案 :可以将多个变量合并到一个对象中,然后使用
AtomicReference
来保证这个对象引用的原子性。
总结
特性 | 描述 |
---|---|
核心 | 无锁的原子操作,通过硬件指令保证"比较-交换"的原子性。 |
优点 | 高性能、非阻塞、避免死锁。 |
缺点 | ABA问题、自旋CPU开销、只能保证一个变量的原子性。 |
应用 | 原子类、乐观锁、无锁数据结构。 |
本质 | 一种乐观并发策略,假设冲突很少发生,发生冲突后再重试。 |
CAS是现代并发编程的基石之一,它使得我们能够在不使用重量级锁的情况下,安全地管理共享数据,极大地提升了多线程程序的性能和可伸缩性。理解CAS是深入理解Java JUC(java.util.concurrent)包或其他语言并发库的关键。
🤔 参考 14
以下是 CAS(Compare and Swap)详解:
一、核心概念与原理
-
基本定义
- 本质:CAS是一种由CPU硬件提供的原子化同步原语,用于实现多线程环境下的无锁同步机制[^1^][^2^]。
- 操作三要素 :
- 内存位置(V):目标变量所在的内存地址;
- 预期原值(A):期望的旧值;
- 新值(B):拟更新的值[^1^][^2^][^3^]。
- 核心语义:"若V的值等于A,则将其更新为B,否则不修改并返回当前值"。这一过程在硬件层面保证原子性[^1^][^2^][^3^]。
-
乐观锁特性
- 无锁设计:与传统互斥锁不同,CAS不阻塞线程,而是通过"尝试-失败重试"机制乐观地假设冲突较少[^2^][^3^]。
- 非阻塞优势:避免了线程上下文切换和挂起带来的性能损耗,适用于低竞争场景[^3^][^5^]。
二、底层实现与技术支撑
-
硬件基础
- CPU指令集 :不同架构提供专用指令实现CAS的原子性:
- x86:
cmpxchg
指令; - ARM:
LDREX/STREX
指令对[^1^][^3^][^5^]。
- x86:
- 原子性保障:通过读-改-写操作的组合指令一次性完成,确保操作不可中断[^1^][^3^]。
- CPU指令集 :不同架构提供专用指令实现CAS的原子性:
-
Java中的封装
- Unsafe类 :Java通过
sun.misc.Unsafe
类的本地方法(如compareAndSwapInt()
)直接调用CPU指令[^1^][^2^][^3^]。 - 间接调用 :由于
Unsafe
类限制访问,通常通过JUC包中的原子类(如AtomicInteger
)间接使用CAS能力[^1^][^2^][^3^]。
- Unsafe类 :Java通过
三、典型应用场景
-
高性能计数器
- 实现方式 :利用
AtomicInteger
的incrementAndGet()
方法,通过自旋+CAS循环实现线程安全递增[^2^][^3^][^4^]。 - 优势:避免锁竞争,显著提升并发性能[^3^][^5^]。
- 实现方式 :利用
-
自定义自旋锁
- 核心逻辑:通过CAS尝试获取锁(如将状态从0改为1),若失败则持续自旋等待[^2^][^4^][^5^]。
- 示例 :
SpinLock
类中通过AtomicReference<Thread>
存储持有者信息[^2^][^4^]。
-
无锁数据结构
- 典型应用:构建非阻塞队列、栈等数据结构,依赖CAS保证节点操作的原子性[^3^][^5^]。
四、关键问题与解决方案
-
ABA问题
- 现象:变量从A→B→A变化后,CAS仍认为未改变,可能导致逻辑错误[^1^][^3^]。
- 解决方案 :
- 版本号机制:结合次数或时间戳判断中间是否发生变更[^3^][^5^];
- AtomicStampedReference:Java提供的带版本控制的引用类[^1^][^3^]。
-
自旋开销与CPU消耗
- 问题:高竞争下频繁重试导致CPU资源浪费[^3^][^5^]。
- 优化策略 :
- 限制重试次数:避免无限循环[^3^][^5^];
- 引入暂停 :使用
Thread.yield()
或LockSupport.parkNanos()
让出CPU[^3^][^5^]。
-
单变量原子性限制
- 局限:CAS仅能保证单个变量的原子性,多变量操作需额外手段[^3^][^5^]。
- 扩展方案 :使用
AtomicReference
包装复合对象,或改用互斥锁[^3^][^5^]。
五、CAS与其他同步机制对比
特性 | CAS | Synchronized/Mutex |
---|---|---|
阻塞行为 | 无阻塞(忙等待) | 阻塞(线程挂起) |
性能 | 低竞争场景更优 | 高竞争场景更优 |
CPU消耗 | 高竞争时较高 | 上下文切换开销大 |
死锁风险 | 无 | 存在 |
适用场景 | 简单操作、低竞争 | 复杂临界区、高竞争 |
综上所述,CAS作为一种轻量级的无锁同步机制,通过硬件原子指令实现了高效的并发控制。其在低竞争场景下性能优异,但也需注意ABA问题、自旋开销及单变量限制等陷阱。合理选择CAS与互斥锁,需根据实际场景权衡性能与复杂度。
🤔 参考 15
CAS(Compare and Swap),即"比较并交换",是一种用于实现多线程同步的原子操作,常用于无锁(lock-free)编程中,是实现线程安全操作的重要机制之一。
一、CAS 的基本原理
CAS 操作包含三个操作数:
- 内存位置(V):要读写的内存变量。
- 预期值(A):线程认为该变量当前应该具有的值。
- 新值(B):线程希望将变量更新为的值。
CAS 的执行逻辑如下:
如果内存位置 V 的当前值等于预期值 A,那么将内存位置 V 的值更新为新值 B;否则不做任何操作。整个操作是原子的,不会被其他线程中断。
用伪代码表示如下:
java
boolean compareAndSwap(int *V, int A, int B) {
if (*V == A) {
*V = B;
return true; // 成功更新
} else {
return false; // 值已被修改,更新失败
}
}
二、CAS 的应用场景
-
原子变量(如 AtomicInteger) : Java 中的
java.util.concurrent.atomic
包提供了基于 CAS 实现的原子类,例如AtomicInteger
、AtomicLong
、AtomicReference
等。 -
无锁队列/栈: 利用 CAS 实现线程安全的队列、栈等数据结构,避免使用传统锁机制,提高并发性能。
-
自旋锁(SpinLock): 基于 CAS 实现的一种轻量级锁机制,适用于锁竞争不激烈的场景。
三、CAS 的优点
- 性能高:相比使用锁机制,CAS 避免了线程阻塞和上下文切换的开销。
- 无死锁:不使用锁,因此不会出现死锁问题。
四、CAS 的缺点
-
ABA 问题: 如果变量 V 的值从 A 被修改为 B,再改回 A,CAS 会误认为它从未被修改过。
解决方案 : 使用版本号(如 Java 中的
AtomicStampedReference
)或时间戳来区分不同状态。 -
只能保证一个变量的原子性 : 如果需要对多个变量进行原子操作,需要额外封装(如使用
AtomicReference
包装多个变量)。 -
自旋开销大: 如果 CAS 多次失败,线程会不断重试(自旋),造成 CPU 资源浪费。
五、Java 中的实现示例
以 AtomicInteger
为例:
java
AtomicInteger atomicInt = new AtomicInteger(0);
// 使用 CAS 实现递增
int oldValue = atomicInt.get();
while (!atomicInt.compareAndSet(oldValue, oldValue + 1)) {
oldValue = atomicInt.get();
}
六、总结
CAS 是一种高效的并发控制机制,适用于读多写少、竞争不激烈的场景。但需注意 ABA 问题、只能操作单个变量等局限性。现代并发编程中,CAS 是构建高性能、无锁数据结构的基础。
CAS 的三大经典问题
CAS 的三大经典问题
1. ABA 问题
问题描述 ABA 问题是 CAS 操作中最经典的问题。它发生在以下情况:
- 线程1读取内存位置 V 的值为 A
- 线程2将值从 A 改为 B,然后又改回 A
- 线程1执行 CAS 操作,发现值仍然是 A,于是操作成功
虽然最终值相同,但中间经历了状态变化,可能导致逻辑错误。
实际场景示例
java
public class ABAProblem {
private AtomicReference<String> stackTop = new AtomicReference<>("A");
public void push(String value) {
String oldTop;
do {
oldTop = stackTop.get();
// 模拟其他操作
} while (!stackTop.compareAndSet(oldTop, value));
}
public String pop() {
String oldTop;
String newTop;
do {
oldTop = stackTop.get();
if (oldTop == null) return null;
// 获取下一个节点
newTop = getNextNode(oldTop);
} while (!stackTop.compareAndSet(oldTop, newTop));
return oldTop;
}
// 线程1:读取栈顶为 "A"
// 线程2:pop() 将栈顶从 "A" -> "B"
// 线程2:push("A") 将栈顶从 "B" -> "A"
// 线程1:CAS 成功,但实际上栈结构已发生变化
}
解决方案
使用 AtomicStampedReference
java
public class ABASolution {
private AtomicStampedReference<String> stackTop =
new AtomicStampedReference<>("A", 0);
public boolean push(String value) {
int[] stampHolder = new int[1];
String oldTop;
int oldStamp;
do {
oldTop = stackTop.get(stampHolder);
oldStamp = stampHolder[0];
// 检查是否有其他修改
} while (!stackTop.compareAndSet(oldTop, value, oldStamp, oldStamp + 1));
return true;
}
public String pop() {
int[] stampHolder = new int[1];
String oldTop;
int oldStamp;
String newTop;
do {
oldTop = stackTop.get(stampHolder);
oldStamp = stampHolder[0];
if (oldTop == null) return null;
newTop = getNextNode(oldTop);
} while (!stackTop.compareAndSet(oldTop, newTop, oldStamp, oldStamp + 1));
return oldTop;
}
}
使用 AtomicMarkableReference
java
public class ABASolution2 {
private AtomicMarkableReference<String> stackTop =
new AtomicMarkableReference<>("A", false);
public boolean push(String value) {
boolean[] markHolder = new boolean[1];
String oldTop;
do {
oldTop = stackTop.get(markHolder);
// 使用标记位来检测变化
} while (!stackTop.compareAndSet(oldTop, value, markHolder[0], !markHolder[0]));
return true;
}
}
2. 循环时间长开销大
问题描述 在高并发场景下,多个线程同时竞争同一个变量,导致大量线程不断重试 CAS 操作,造成 CPU 资源浪费。
性能影响
java
public class HighContentionExample {
private AtomicInteger counter = new AtomicInteger(0);
public void highContentionMethod() {
// 在高并发下,这个循环可能导致大量 CPU 消耗
while (!counter.compareAndSet(counter.get(), counter.get() + 1)) {
// 空转,消耗 CPU
}
}
}
解决方案
- 退避策略 (Backoff)
java
public class BackoffStrategy {
private AtomicInteger counter = new AtomicInteger(0);
private final int maxDelay = 1000; // 最大延迟 1ms
public void incrementWithBackoff() {
int delay = 1;
while (true) {
int current = counter.get();
int next = current + 1;
if (counter.compareAndSet(current, next)) {
break; // 成功退出
}
// 指数退避
try {
Thread.sleep(0, delay); // 纳秒级休眠
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
delay = Math.min(delay * 2, maxDelay);
}
}
}
- 使用 LongAdder (Java 8+)
java
import java.util.concurrent.atomic.LongAdder;
public class LowContentionSolution {
private LongAdder counter = new LongAdder();
public void increment() {
counter.increment(); // 内部使用分段计数,减少竞争
}
public long getCount() {
return counter.sum();
}
}
- 分散热点
java
public class HotspotDistribution {
private final int CONCURRENCY_LEVEL = Runtime.getRuntime().availableProcessors();
private AtomicInteger[] counters;
public HotspotDistribution() {
counters = new AtomicInteger[CONCURRENCY_LEVEL];
for (int i = 0; i < CONCURRENCY_LEVEL; i++) {
counters[i] = new AtomicInteger(0);
}
}
public void increment() {
int index = Thread.currentThread().hashCode() % CONCURRENCY_LEVEL;
if (index < 0) index = -index;
counters[index].incrementAndGet();
}
public int getTotal() {
int sum = 0;
for (AtomicInteger counter : counters) {
sum += counter.get();
}
return sum;
}
}
3. 只能保证一个共享变量的原子操作
问题描述 CAS 只能保证单个变量的原子性,对于需要同时更新多个相关变量的场景无法保证一致性。
典型场景
java
public class MultiVariableProblem {
private AtomicInteger x = new AtomicInteger(0);
private AtomicInteger y = new AtomicInteger(0);
// 需要原子性地更新 x 和 y
public void updateBoth(int newX, int newY) {
// 这两个操作不是原子的
x.set(newX);
y.set(newY);
// 在 set(newX) 和 set(newY) 之间,其他线程可能看到不一致的状态
}
public void checkConsistency() {
int currentX = x.get();
int currentY = y.get();
// 这里可能看到 x 和 y 的不一致状态
}
}
解决方案
- 使用 AtomicReference 封装对象
java
public class MultiVariableSolution1 {
static class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
private AtomicReference<Point> point = new AtomicReference<>(new Point(0, 0));
public void updateBoth(int newX, int newY) {
Point current;
Point newPoint;
do {
current = point.get();
newPoint = new Point(newX, newY);
} while (!point.compareAndSet(current, newPoint));
}
public int getX() {
return point.get().x;
}
public int getY() {
return point.get().y;
}
}
- 使用锁机制
java
public class MultiVariableSolution2 {
private int x = 0;
private int y = 0;
private final Object lock = new Object();
public void updateBoth(int newX, int newY) {
synchronized(lock) {
x = newX;
y = newY;
}
}
public void checkConsistency() {
synchronized(lock) {
int currentX = x;
int currentY = y;
// 保证看到一致的状态
}
}
}
- 使用 VarHandle (Java 9+)
java
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class MultiVariableSolution3 {
private static class State {
int x;
int y;
}
private volatile State state = new State();
private static final VarHandle STATE_HANDLE;
static {
try {
STATE_HANDLE = MethodHandles.lookup()
.findVarHandle(MultiVariableSolution3.class, "state", State.class);
} catch (Exception e) {
throw new Error(e);
}
}
public void updateBoth(int newX, int newY) {
State current;
State newState = new State();
newState.x = newX;
newState.y = newY;
do {
current = state;
} while (!STATE_HANDLE.compareAndSet(this, current, newState));
}
}
综合解决方案示例
完整的线程安全计数器
java
public class CompleteCounter {
private final LongAdder baseCount = new LongAdder();
private final AtomicStampedReference<Long> lastUpdate =
new AtomicStampedReference<>(0L, 0);
public void increment() {
baseCount.increment();
// 原子更新最后更新时间戳
int[] stampHolder = new int[1];
Long oldValue;
int oldStamp;
do {
oldValue = lastUpdate.get(stampHolder);
oldStamp = stampHolder[0];
} while (!lastUpdate.compareAndSet(oldValue, System.currentTimeMillis(),
oldStamp, oldStamp + 1));
}
public long getCount() {
return baseCount.sum();
}
public long getLastUpdateTime() {
return lastUpdate.getReference();
}
}
总结
问题 | 描述 | 解决方案 |
---|---|---|
ABA 问题 | 值从 A→B→A,CAS 误认为无变化 | 使用版本号(AtomicStampedReference) |
循环开销大 | 高竞争下大量线程空转 | 退避策略、LongAdder、分散热点 |
单变量限制 | 无法保证多变量原子性 | 对象封装、锁机制、VarHandle |
理解这三大问题并选择合适的解决方案,对于编写正确、高效的无锁并发程序至关重要。在实际开发中,应根据具体场景权衡各种方案的优缺点。
CAS 的三大经典问题: 1.ABA 问题 ; 2.循环时间长开销大 ; 3.只能保证一个共享变量的原子操作
好的,CAS(Compare and Swap)虽然强大,但也存在三个经典的棘手问题。下面我们来详细解析这三大经典问题。
问题一:ABA 问题
什么是 ABA 问题?
ABA 问题是 CAS 操作中最著名、最经典的问题。它的产生过程如下:
- 线程 1 从内存位置 V 读取值为 A
- 线程 1 被挂起(时间片用完、调度等)
- 线程 2 开始执行,将 V 的值从 A 改为 B
- 线程 2 继续执行,又将 V 的值从 B 改回 A
- 线程 1 恢复执行,进行 CAS 操作:
- 检查 V 的值是否还是 A(确实是 A)
- CAS 操作成功,将值更新为新值
问题在于 :虽然 CAS 在技术上成功了,但 V 的值在期间已经经历了 A → B → A 的变化,这个"中间状态"被线程 1 完全忽略了。
ABA 问题的危害
用一个现实生活中的例子:
- 你把一瓶水放在桌上,离开了一会
- 你的同事喝光了这瓶水,然后重新灌满了清水
- 你回来后,看到还是一瓶"水",以为没人动过,就继续喝
虽然看起来都是"一瓶水",但内容已经发生了变化。
在编程中的具体危害:
- 在链表、栈等数据结构中,节点地址可能被重用,导致数据结构损坏
- 在版本控制中,无法感知中间的状态变化
- 在财务系统中,资金可能经历了多次流转后回到原值,但交易历史被忽略
解决方案
1. 添加版本号(Version Stamp) 每次修改都递增一个版本号,CAS 时同时检查值和版本号。
Java 中的实现:AtomicStampedReference
java
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
public static void main(String[] args) {
// 初始值为 100,版本号为 0
AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(100, 0);
int oldValue = 100;
int newValue = 200;
int oldStamp = atomicStampedRef.getStamp(); // 获取当前版本号
// 线程1:尝试CAS,但被挂起
// 线程2:修改值 100 -> 150,版本号 0 -> 1
boolean success2 = atomicStampedRef.compareAndSet(100, 150, 0, 1);
// 线程2:再次修改值 150 -> 100,版本号 1 -> 2
boolean success3 = atomicStampedRef.compareAndSet(150, 100, 1, 2);
// 线程1:恢复执行,尝试CAS
// 虽然值还是100,但版本号已经从0变成2,所以CAS失败!
boolean success1 = atomicStampedRef.compareAndSet(oldValue, newValue, oldStamp, oldStamp + 1);
System.out.println("Thread1 CAS: " + success1); // false
System.out.println("Current value: " + atomicStampedRef.getReference()); // 100
System.out.println("Current stamp: " + atomicStampedRef.getStamp()); // 2
}
}
2. 使用布尔标记 Java 中的 AtomicMarkableReference
用一个布尔值来标记是否被修改过。
java
import java.util.concurrent.atomic.AtomicMarkableReference;
public class MarkableSolution {
public static void main(String[] args) {
AtomicMarkableReference<Integer> atomicMarkableRef =
new AtomicMarkableReference<>(100, false);
// 类似的CAS操作,同时检查值和标记位
boolean success = atomicMarkableRef.compareAndSet(100, 200, false, true);
}
}
问题二:循环时间长开销大
问题描述
在高并发环境下,当多个线程同时竞争同一个资源时:
- 线程执行 CAS 操作失败后,会立即重试(自旋)
- 如果竞争非常激烈,大量线程会不断循环重试
- 导致 CPU 资源被大量消耗在无用的循环上
具体表现
java
public class SpinProblem {
private AtomicInteger counter = new AtomicInteger(0);
public void highContentionMethod() {
// 在高并发环境下,这个循环可能执行很多次
while (!counter.compareAndSet(counter.get(), counter.get() + 1)) {
// 空循环,消耗CPU
}
}
}
解决方案
1. 引入退避机制 失败后不是立即重试,而是等待一小段时间:
java
public class BackoffSolution {
private AtomicInteger counter = new AtomicInteger(0);
public void casWithBackoff() {
int backoffTime = 1; // 初始退避时间
while (true) {
int current = counter.get();
int next = current + 1;
if (counter.compareAndSet(current, next)) {
break; // 成功,退出循环
} else {
// 失败,执行退避
try {
Thread.sleep(backoffTime);
backoffTime = Math.min(backoffTime * 2, 100); // 指数退避,但有上限
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}
2. 使用 LongAdder(JDK8+) 在超高并发统计场景下,LongAdder
比 AtomicLong
性能更好:
java
import java.util.concurrent.atomic.LongAdder;
public class LongAdderSolution {
private LongAdder counter = new LongAdder();
public void increment() {
counter.increment(); // 内部使用分段CAS,减少竞争
}
public long getValue() {
return counter.sum();
}
}
3. 切换为锁机制 当检测到竞争过于激烈时,可以降级使用传统的锁:
java
public class AdaptiveSolution {
private AtomicInteger casCounter = new AtomicInteger(0);
private final Object lock = new Object();
private int lockCounter = 0;
private int failureCount = 0;
public void adaptiveIncrement() {
if (failureCount > 1000) { // 如果失败次数太多,使用锁
synchronized(lock) {
lockCounter++;
}
} else {
if (!casCounter.compareAndSet(casCounter.get(), casCounter.get() + 1)) {
failureCount++;
// 可能递归调用或其他处理
}
}
}
}
问题三:只能保证一个共享变量的原子操作
问题描述
单个 CAS 指令只能保证一个内存位置 的原子性。如果需要同时原子地更新多个相关变量,CAS 就无能为力了。
典型场景
java
public class MultiVariableProblem {
private AtomicInteger x = new AtomicInteger(0);
private AtomicInteger y = new AtomicInteger(0);
// 错误:这两个CAS操作不是原子的
public void updateBoth(int newX, int newY) {
x.compareAndSet(x.get(), newX); // 第一步
y.compareAndSet(y.get(), newY); // 第二步
// 在两个CAS操作之间,其他线程可能看到不一致的状态
}
}
解决方案
1. 使用 AtomicReference 包装对象 将多个变量封装到一个对象中,然后对整个对象进行 CAS:
java
import java.util.concurrent.atomic.AtomicReference;
public class MultiVariableSolution {
static class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
private AtomicReference<Point> pointRef =
new AtomicReference<>(new Point(0, 0));
public void updateBoth(int newX, int newY) {
Point current, next;
do {
current = pointRef.get();
next = new Point(newX, newY);
} while (!pointRef.compareAndSet(current, next));
}
public int getX() {
return pointRef.get().x;
}
public int getY() {
return pointRef.get().y;
}
}
2. 使用锁机制 对于复杂的多变量原子操作,有时使用锁更简单可靠:
java
public class LockSolution {
private int x = 0;
private int y = 0;
private final Object lock = new Object();
public void updateBoth(int newX, int newY) {
synchronized(lock) {
x = newX;
y = newY;
}
}
}
3. 使用数组 + AtomicReferenceArray 如果需要原子地更新数组中的多个元素:
java
import java.util.concurrent.atomic.AtomicReferenceArray;
public class ArraySolution {
private AtomicReferenceArray<String> array =
new AtomicReferenceArray<>(10);
// 虽然不能原子更新多个元素,但可以原子更新单个元素
public void updateElement(int index, String newValue) {
array.set(index, newValue);
}
}
总结对比
问题 | 描述 | 解决方案 |
---|---|---|
ABA 问题 | 值经历了 A→B→A 变化,CAS 无法感知中间状态 | 1. AtomicStampedReference (版本号) 2. AtomicMarkableReference (标记位) |
循环开销大 | 高竞争下大量线程自旋,消耗 CPU | 1. 退避机制 2. LongAdder 分段 3. 竞争激烈时降级为锁 |
多变量原子性 | 单个 CAS 只能保证一个变量的原子性 | 1. 使用 AtomicReference 包装对象 2. 使用锁 3. 重新设计数据结构 |
实践建议
- 低竞争场景:优先使用 CAS,性能最好
- 高竞争场景 :考虑使用
LongAdder
或退避机制 - 状态复杂场景 :使用
AtomicStampedReference
或直接使用锁 - 需要事务性操作 :考虑将多个变量封装为不可变对象,使用
AtomicReference
理解这三大问题及其解决方案,有助于在实际开发中更好地运用 CAS 技术,在保证线程安全的同时获得最佳性能。
乐观锁 与 悲观锁
我们来详细探讨乐观锁 与悲观锁这两种重要的并发控制策略。
1. 核心思想对比
悲观锁 (Pessimistic Locking) 思想:假设最坏的情况会发生。"我认为只要我访问数据,别人就很可能也会来修改它,所以我要先加锁,确保只有我能操作。"
比喻:
- 就像你把日记本锁在抽屉里,认为只要拿出来别人就可能会看
- 就像停车场只有一个出口,车辆必须排队依次通过
乐观锁 (Optimistic Locking) 思想:假设最好的情况会发生。"我认为别人不太会在我操作的时候修改数据,所以我不加锁,先操作,提交时再检查有没有冲突。"
比喻:
- 就像多人编辑在线文档,各自编辑自己的副本,最后合并时解决冲突
- 就像Git版本控制,各自提交代码,合并时处理冲突
2. 实现机制对比
悲观锁的实现方式
数据库层面:
sql
-- 使用 SELECT ... FOR UPDATE 锁定记录
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 悲观锁
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
Java 层面:
java
// 使用 synchronized
public synchronized void transfer(int from, int to, int amount) {
// 转账操作
}
// 使用 ReentrantLock
private final Lock lock = new ReentrantLock();
public void transfer(int from, int to, int amount) {
lock.lock();
try {
// 转账操作
} finally {
lock.unlock();
}
}
乐观锁的实现方式
数据库层面(版本号机制):
sql
-- 添加 version 字段
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 123 AND version = 5; -- 检查版本号
-- 如果受影响行数为0,说明版本号已变化,需要重试
Java 层面(CAS 操作):
java
// 使用 AtomicInteger
private AtomicInteger balance = new AtomicInteger(1000);
public boolean withdraw(int amount) {
int current, next;
do {
current = balance.get();
if (current < amount) {
return false; // 余额不足
}
next = current - amount;
} while (!balance.compareAndSet(current, next)); // CAS操作
return true;
}
// 使用 AtomicStampedReference 解决ABA问题
private AtomicStampedReference<Integer> account =
new AtomicStampedReference<>(1000, 0);
public boolean transfer(int amount) {
int[] stampHolder = new int[1];
int current, next, currentStamp, nextStamp;
do {
current = account.get(stampHolder);
currentStamp = stampHolder[0];
if (current < amount) return false;
next = current - amount;
nextStamp = currentStamp + 1;
} while (!account.compareAndSet(current, next, currentStamp, nextStamp));
return true;
}
3. 工作流程对比
悲观锁工作流程
markdown
1. 申请锁
↓
2. 获取锁成功? → 失败:阻塞等待
↓ 成功
3. 执行业务操作
↓
4. 提交事务,释放锁
乐观锁工作流程
markdown
1. 读取数据(包括版本号)
↓
2. 执行业务逻辑(计算新值)
↓
3. 尝试更新(检查版本号)
↓
4. 版本一致? → 否:重试或失败处理
↓ 是
5. 更新成功
4. 优缺点分析
悲观锁的优点
- ✅ 简单可靠:逻辑直接,容易理解
- ✅ 强一致性:保证数据操作的绝对串行化
- ✅ 避免冲突:从根本上防止并发冲突
- ✅ 适合长事务:对于执行时间长的操作很有效
悲观锁的缺点
- ❌ 性能开销:加锁、释放锁需要额外开销
- ❌ 死锁风险:可能产生死锁,需要额外处理
- ❌ 并发度低:阻塞其他线程,降低系统吞吐量
- ❌ ** scalability 差**:不适合高并发场景
乐观锁的优点
- ✅ 高并发:读操作不会被阻塞,提高吞吐量
- ✅ 无死锁:不使用锁,避免死锁问题
- ✅ 性能好:在低冲突场景下性能优异
- ✅ 适合读多写少:大多数场景都是读多写少
乐观锁的缺点
- ❌ ABA 问题:需要额外机制解决
- ❌ 高冲突时性能差:大量重试消耗 CPU
- ❌ 实现复杂:需要处理重试逻辑和冲突解决
- ❌ 不适合长事务:重试次数可能很多
5. 适用场景
适合使用悲观锁的场景
- 写操作频繁:冲突发生概率很高的场景
- 临界区复杂:操作执行时间较长
- 数据一致性要求极高:不允许任何冲突发生
- 简单业务逻辑:不想处理复杂的重试机制
具体例子:
- 银行核心系统的账户余额修改
- 库存管理中稀缺商品的抢购
- 票务系统的座位锁定
适合使用乐观锁的场景
- 读多写少:大部分操作是读取,很少修改
- 低冲突环境:并发写入的概率较低
- 高性能要求:需要支持高并发访问
- 短事务:业务操作执行时间短
具体例子:
- 社交媒体的点赞计数
- 文章的阅读量统计
- 用户个人信息的更新
- 购物车的商品数量修改
6. 实战示例
库存管理的两种实现
悲观锁实现:
java
@Service
public class InventoryServicePessimistic {
@Transactional
public boolean deductStock(Long productId, Integer quantity) {
// 悲观锁:查询时锁定记录
Product product = productRepository.findByIdWithLock(productId);
if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
productRepository.save(product);
return true;
}
return false;
}
}
乐观锁实现:
java
@Service
public class InventoryServiceOptimistic {
public boolean deductStock(Long productId, Integer quantity) {
int retryCount = 0;
final int MAX_RETRY = 3;
while (retryCount < MAX_RETRY) {
Product product = productRepository.findById(productId);
if (product.getStock() < quantity) {
return false;
}
int updated = productRepository.updateStockWithVersion(
productId, quantity, product.getVersion());
if (updated > 0) {
return true; // 更新成功
}
retryCount++;
// 可选:加入指数退避
try { Thread.sleep(10 * retryCount); } catch (InterruptedException e) { break; }
}
return false; // 重试多次后失败
}
}
7. 总结对比表格
特性 | 悲观锁 | 乐观锁 |
---|---|---|
核心思想 | 先加锁,后操作 | 先操作,后检查 |
实现方式 | synchronized 、ReentrantLock 、SELECT FOR UPDATE |
CAS、版本号、时间戳 |
性能 | 低并发下较好,高并发下差 | 低冲突下极好,高冲突下差 |
数据一致性 | 强一致性 | 最终一致性 |
死锁 | 可能发生 | 不会发生 |
适用场景 | 写多读少、高冲突、长事务 | 读多写少、低冲突、短事务 |
开发复杂度 | 简单 | 相对复杂 |
扩展性 | 较差 | 较好 |
8. 选择建议
- 默认选择乐观锁:在大多数现代应用中,读操作远多于写操作,乐观锁是更好的选择
- 冲突频繁用悲观锁:当业务逻辑注定会产生大量冲突时,使用悲观锁
- 考虑业务特性:根据数据的重要性和一致性要求做出选择
- 可以混合使用:在同一系统中,不同模块可以根据特性使用不同的锁策略
现代趋势:随着分布式系统和微服务的普及,基于版本的乐观锁机制越来越受欢迎,因为它更适合分布式环境,能提供更好的扩展性和性能。