文章目录
CAS(Compare-And-Swap,比较并交换)是一种用于实现多线程同步的无锁编程技术,主要用于解决并发编程中的数据一致性问题。它是一种原子操作,通常用于实现线程安全的算法,而无需使用传统的锁机制(如互斥锁)。
在并发编程中,代码的“原子性”指的是一个操作在执行过程中不会被其他线程中断,即要么全部完成,要么全部不完成。
如果一个操作不是原子性的,那么在多线程环境中可能会导致数据不一致或其他并发问题。
CAS 伪代码
这里的代码不是原⼦的,真实的 CAS 是⼀个原⼦的硬件指令完成的,这个伪代码只是辅助理解 CAS 的⼯作流程。
boolean CAS(address, expectValue, newValue) {
if (&address == expectedValue) {
&address = newValue;
return true;
} else {
return false;
}
}
- CAS操作涉及三个参数:
- 内存位置(address):要更新的变量在内存中的地址。
- 预期值(expectedValue):期望内存位置当前的值。
- 新值(newValue):如果内存位置的值等于预期值,则将该位置更新为新值。
- CAS 的操作流程如下:
- 比较内存中的值
address
和预期值expectedValue
- 如果相等,则将内存中的值
address
更新为新值newValue
,并返回成功; - 如果不相等,则不进行任何修改,并返回失败。
- 比较内存中的值
注意:
当多个线程同时对某个资源进⾏ CAS 操作,只能有⼀个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。
CAS 可以视为是⼀种乐观锁,或者可以理解成 CAS 是乐观锁的⼀种实现⽅式。
两种典型的不是“原⼦性”的代码:
check and set
(if 判定然后设定值)
public class NonAtomicCheckAndSet {
private int value;
public void setValueIf(int expectedValue, int newValue) {
if (value == expectedValue) { // 非原子性检查
value = newValue; // 非原子性更新
}
}
public int getValue() {
return value;
}
public static void main(String[] args) {
NonAtomicCheckAndSet example = new NonAtomicCheckAndSet();
example.setValueIf(0, 1); // 假设value初始值为0
}
}
问题:
在多线程环境下,if
判断和赋值操作不是原子性的。可能存在以下问题:
- 线程1读取
value
为0,判断条件成立。 - 在线程1执行
value = newValue
之前,线程2将value
修改为其他值。 - 线程1继续执行
value = newValue
,此时value
的值可能已经不符合预期条件了。
解决方案:
使用AtomicInteger
的compareAndSet
方法来实现原子性操作:
public final boolean compareAndSet(int expect, int update)
expect
是当前值的预期值,update
是想要更新的新值。- 如果当前值等于预期值,则将当前值更新为新值,并返回
true
; - 否则不进行更新,返回
false
。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCheckAndSet {
private AtomicInteger value = new AtomicInteger(0);
public void setValueIf(int expectedValue, int newValue) {
value.compareAndSet(expectedValue, newValue); // 原子性操作
}
public int getValue() {
return value.get();
}
public static void main(String[] args) {
AtomicCheckAndSet example = new AtomicCheckAndSet();
example.setValueIf(0, 1); // 假设value初始值为0
}
}
read and update
(i++)
public class NonAtomicReadAndUpdate {
private int counter;
public void increment() {
counter++; // 非原子性操作
}
public int getCounter() {
return counter;
}
public static void main(String[] args) {
NonAtomicReadAndUpdate example = new NonAtomicReadAndUpdate();
example.increment(); // 假设counter初始值为0
}
}
问题:
在多线程环境下,counter++
操作不是原子性的。counter++
实际上是一个复合操作,包括以下步骤:
- 读取
counter
的当前值。 - 将当前值加1。
- 将新值写回
counter
。
所以,可能会出现以下问题:
- **线程1 **读取
counter
的值为0。 - **线程2 **读取
counter
的值也为0。 - 线程1 将
counter
的值加1,更新为1。 - 线程2 也将
counter
的值加1,更新为1。
最终,counter
的值可能只有1,而不是预期的2。
解决方案:
使用AtomicInteger
的getAndIncrement
方法来实现原子性操作:
**incrementAndGet**
:相当于 ++i
**getAndIncrement**
:相当于 i++
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicReadAndUpdate {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.getAndIncrement(); // 原子性操作
}
public int getCounter() {
return counter.get();
}
public static void main(String[] args) {
AtomicReadAndUpdate example = new AtomicReadAndUpdate();
example.increment(); // 假设counter初始值为0
}
}
CAS 是怎么实现的?
针对不同的操作系统,JVM ⽤到了不同的 CAS 实现原理:
- Java 层面:
**Unsafe**
类- 在 Java 中,CAS 操作通常通过
sun.misc.Unsafe
类提供的本地方法来实现。这个类提供了低级别的内存操作和原子操作支持。 Unsafe
类中的compareAndSwapInt
,compareAndSwapLong
, 和compareAndSwapObject
方法分别用于对int
、long
、对象类型的变量进行 CAS 操作。
- 在 Java 中,CAS 操作通常通过
public final boolean compareAndSet(int expectedValue, int newValue) {
return unsafe.compareAndSwapInt(this, valueOffset, expectedValue, newValue);
}
- JVM 层面:
**Atomic::cmpxchg**
Unsafe
类中的 CAS 方法最终调用了 JVM 内部的Atomic::cmpxchg
函数。这个函数是 JVM 针对不同操作系统和硬件平台的具体实现。- JVM 通过 JNI(Java Native Interface)与底层操作系统和硬件进行交互,并根据不同的操作系统和处理器架构实现相应的 CAS 操作。
- 操作系统和硬件层面:汇编指令和 CPU 硬件支持
- 在最底层,
Atomic::cmpxchg
函数依赖于特定 CPU 架构提供的原子指令。例如:- **x86/x64 架构:**使用
CMPXCHG
指令。
- **x86/x64 架构:**使用
- 在最底层,
LOCK CMPXCHG [destination], source
CMPXCHG
指令比较目标地址的值和累加器(通常是EAX
或RAX
寄存器)中的值。如果相等,则将源操作数写入目标地址;否则,将目标地址的值加载到累加器中。LOCK
前缀确保该指令在整个执行过程中不会被其他处理器中断,从而保证了原子性。
2. **ARM 架构:**使用 `LDREX` 和 `STREX` 指令组合。
LDREX R1, [R0] ; 加载目标地址的值到 R1 并标记该地址为独占访问
... ; 执行其他操作
STREX R2, R3, [R0] ; 如果目标地址仍处于独占状态,则将 R3 的值存储到目标地址并设置 R2 为 0;否则设置 R2 为非零值
LDREX
指令加载一个值并标记该地址为独占访问。STREX
指令尝试将一个新值存储到该地址,但如果该地址不再处于独占状态,则操作失败。
这些指令确保了在多核或多线程环境下,对共享内存的操作是原子性的。
总结:
- Java层面:通过
Unsafe
类提供的 CAS 方法,Java代码可以实现高效的并发控制。 - JVM层面:
Unsafe
类的 CAS 方法调用 JVM 的Atomic::cmpxchg
方法,这是一个平台相关的实现。 - 硬件层面:
Atomic::cmpxchg
方法最终依赖于 CPU 提供的原子操作指令(如CMPXCHG
),并使用lock
前缀确保操作的原子性。
正是因为有了硬件和操作系统的支持,软件层面才能实现这些高效的并发控制机制。
CAS 有哪些应用?
实现原子类
标准库中提供了java.util.concurrent.atomic
包,⾥⾯的类都是基于这种⽅式来实现的。典型的就是AtomicInteger
类,其中的
getAndIncrement
:相当于i++
操作incrementAndGet
:相当于++i
操作getAndDecrement
:相当于i--
操作decrementAndGet
:相当于--i
操作getAndAdd
:相当于累加
操作
伪代码解释:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
假设两个线程同时调⽤getAndIncrement
:
- 两个线程都读取 value 的值到 oldValue 中(oldValue 是⼀个局部变量,在栈上,每个线程有⾃⼰的栈)
- **线程1 **先执⾏ CAS 操作。由于 oldValue 和 value 的值相同,直接进⾏对 value 赋值
- CAS 是直接读写内存的,⽽不是操作寄存器;
- CAS 的读内存、⽐较、写内存操作是⼀条硬件指令,是原⼦的。
- **线程2 **再执⾏ CAS 操作,第⼀次 CAS 的时候发现 oldValue 和 value 不相等,不能进⾏赋值,因此需要进⼊循环,在循环⾥重新读取 value 的值赋给 oldValue。
- **线程2 **接下来第⼆次执⾏ CAS,此时 oldValue 和 value 相同,于是直接执⾏赋值操作。
- 线程1 和 线程2 返回各⾃的 oldValue 的值即可。
通过形如上述代码就可以实现⼀个原⼦类,不需要使⽤重量级锁,就可以⾼效的完成多线程的⾃增操作。
本来
check and set
这样的操作在代码⻆度不是原⼦的,但是在硬件层⾯上可以让⼀条指令完成这个操作,也就变成原⼦的了 。
实现自旋锁
自旋锁就是通过 CAS 实现的,即通过循环让线程不断检查某个条件是否满足(即“自旋”),直到获取锁为止。
自旋锁伪代码:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有,
// 如果这个锁已经被别的线程持有,那么就自旋等待,
// 如果这个锁没有被别的线程持有,那么就把 owner 设为当前尝试加锁的线程。
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
CAS 的 ABA 问题
ABA问题是指在多线程环境下,一个变量的值可能在CAS操作期间被其他线程修改多次,导致CAS操作误认为变量的值没有发生变化,从而引发错误。
ABA 场景描述
假设有一个共享变量 A
,初始值为 A0
:
- 线程 T1 读取到
A = A0
。 - 在 T1 进行 CAS 操作之前,另一个线程 T2 修改了
A
的值:- T2 先将
A
从A0
改为B
; - 然后 T2 又将
A
从B
改回A0
。
- T2 先将
- 当 T1 执行 CAS 操作时,它会发现
A
的值仍然是A0
,因此 CAS 操作成功,但实际上A
已经经历了A0 -> B -> A0
的变化。
这种情况下,T1 并不知道 A
曾经被修改过,这可能导致程序逻辑错误。
ABA 问题引来的 BUG
⼤部分的情况下,T2 线程这样的⼀个反复横跳改动,对于 T1 修改 A 的值是没有影响的,但是不排除⼀些特殊情况,场景如下:
假设:
滑稽老铁 A 有 100 元存款,想在早上 11 点钟给老铁 B 转 50 元,但是他可能会没有时间,所以和老铁 C 约定好:在 11 点时,查一下我的存款余额,
- 如果是 50 元,则说明已经转过账了,就不用麻烦老铁 C 了;
- 如果是 100 元,则说明还没有转账,需要老铁 C 拿 A 的账号给老铁 B 转账。
如果使用 CAS 的方式,则可能会出现异常情况:
- 在 11 点之前,A 已经给 B 转过账了,此刻余额为 50 元;
- 但是,同样在 11 点之前,A 的公司给 A 转了一笔账,刚好是 50 元,此刻,A 的余额为 100 元;
- 11 点时,C 查看 A 的余额,发现是 100 元,所以向 B 转账 50**(重复转账)**。
从上面场景可以看出,多给 B 转了 50 元,这就出现了 BUG。
解决方案
通过引入一个版本号字段来跟踪变量的变化情况。每次对变量进行修改时,版本号也会相应增加。这样,在进行 CAS 操作时不仅比较变量的值,还要比较版本号。
版本号可以使用自增的数值,也可以使用时间戳,只要可以区分变量是否改变即可。
Java 提供了一个内置的类 **AtomicStampedReference**
,它可以同时存储一个引用和一个整数戳(stamp)
。通过比较引用和戳的值,可以有效避免 ABA 问题。
CAS 的优缺点
- 优点:
- 无锁机制:CAS操作不需要锁,因此不会导致线程阻塞,提高了并发性能。
- 避免死锁:由于没有锁,因此不会出现死锁问题。
- 减少上下文切换:无锁机制减少了线程之间的上下文切换,提高了程序的效率。
- 缺点:
- ABA问题
- 循环时间长:如果多个线程竞争同一个变量,CAS操作可能会进入长时间的自旋状态,导致CPU资源浪费。
- 仅适用于单个变量操作:CAS操作通常只能用于单个变量的原子操作,对于多个变量的复合操作,需要额外的机制来保证原子性。