CAS(Compare-And-Swap,比较并交换) 是一种硬件级别的原子操作,用于实现多线程环境下的无锁同步。它通过比较内存中的值是否等于预期值(expected
),如果相等,则将内存中的值更新为新值(newValue
);否则,不做任何操作。
CAS 是现代并发编程中非常重要的基础技术,广泛应用于 Java 的 java.util.concurrent
包(如 Atomic
类)和其他高性能并发框架中。
CAS 的工作原理
1. 基本步骤
CAS 操作的核心逻辑如下:
- 比较内存中的值(
currentValue
)是否等于预期值(expectedValue
)。 - 如果相等,则将内存中的值更新为新值(
newValue
)。 - 如果不相等,则不更新,并返回失败。
2. 伪代码
以下是 CAS 的伪代码实现:
boolean compareAndSwap(memoryAddress, expectedValue, newValue) {
if (memoryAddress.value == expectedValue) {
memoryAddress.value = newValue;
return true; // 更新成功
} else {
return false; // 更新失败
}
}
3. 硬件支持
CAS 操作依赖于 CPU 的指令集支持,例如:
- x86 架构:
CMPXCHG
指令。 - ARM 架构:
LDREX
和STREX
指令。 - 其他架构:类似的原子指令。
这些指令能够在硬件层面保证比较和更新操作是不可分割的(原子的)。
CAS 的优点
-
无锁同步:
- CAS 是一种无锁算法,避免了传统锁(如
synchronized
或ReentrantLock
)带来的线程阻塞和上下文切换开销。
- CAS 是一种无锁算法,避免了传统锁(如
-
高性能:
- CAS 操作直接由硬件支持,性能非常高,适合高并发场景。
-
简单易用:
- CAS 提供了简单的原子操作接口,适合实现线程安全的数据结构(如
AtomicInteger
、ConcurrentHashMap
)。
- CAS 提供了简单的原子操作接口,适合实现线程安全的数据结构(如
CAS 的缺点
-
ABA 问题:
-
如果内存中的值从
A
变为B
,然后又变回A
,CAS 操作会认为值没有变化,从而导致错误。 -
解决方法:
- 使用版本号(如
AtomicStampedReference
)来标记值的变化。 - 每次更新时同时更新版本号,避免 ABA 问题。
- 使用版本号(如
-
-
自旋开销:
- 如果 CAS 操作失败,通常会采用自旋重试的方式(不断尝试更新值),在高竞争场景下可能导致 CPU 资源浪费。
-
只能操作单个变量:
-
CAS 只能保证单个变量的原子性,无法直接操作多个变量。
-
解决方法:
- 使用
AtomicReference
或其他高级工具(如StampedLock
)。
- 使用
-
-
硬件依赖:
- CAS 操作依赖于底层硬件指令集支持,不同架构的实现可能有所不同。
CAS 的应用场景
- Java 并发包中的
Atomic
类
- Java 提供了
java.util.concurrent.atomic
包,其中的类(如AtomicInteger
、AtomicBoolean
、AtomicReference
)都基于 CAS 实现。 - 示例:
AtomicInteger
的compareAndSet
方法。
AtomicInteger atomicInteger = new AtomicInteger(0);
boolean success = atomicInteger.compareAndSet(0, 1); // 如果当前值是 0,则更新为 1
System.out.println(success); // 输出 true
-
高性能队列
- 无锁队列(如
ConcurrentLinkedQueue
)使用 CAS 来实现线程安全的入队和出队操作。
- 无锁队列(如
-
线程池
- Java 的
ThreadPoolExecutor
使用 CAS 来更新线程池的状态。
- Java 的
-
计数器
- 高并发场景下的计数器(如
AtomicLong
)使用 CAS 实现线程安全的递增和递减。
- 高并发场景下的计数器(如
CAS 的实现细节(以 Java 为例)
1. Unsafe
类
- Java 的
Atomic
类底层依赖于sun.misc.Unsafe
类中的 CAS 方法。 - 示例:
compareAndSwapInt
方法。
public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int newValue);
-
obj
:要操作的对象。offset
:对象中变量的内存偏移量。expected
:预期值。newValue
:要更新的新值。
2. 内存偏移量
Unsafe
类通过内存偏移量直接操作对象的字段。- 示例:获取字段的内存偏移量。
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);
}
}
3. CAS 操作
AtomicInteger
的compareAndSet
方法实现:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
CAS 的优缺点对比
特性 | 优点 | 缺点 |
---|---|---|
线程安全性 | 无锁同步,避免线程阻塞和上下文切换。 | 在高竞争场景下可能导致自旋开销。 |
性能 | 硬件级别的原子操作,性能非常高。 | 依赖硬件指令集,不同架构的实现可能有所不同。 |
适用场景 | 适合单变量的原子操作,如计数器、标志位等。 | 无法直接操作多个变量,需借助其他工具(如 AtomicReference )。 |
问题 | 简单高效,适合高并发场景。 | 存在 ABA 问题,需要额外处理(如版本号)。 |
CAS 的扩展:ABA 问题
什么是 ABA 问题?
- ABA 问题是指:如果内存中的值从
A
变为B
,然后又变回A
,CAS 操作会认为值没有变化,从而导致错误。
解决 ABA 问题的方法
-
版本号机制:
- 在值的基础上附加一个版本号,每次更新值时同时更新版本号。
- 示例:
AtomicStampedReference
。
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 1);
int[] stampHolder = new int[1];
int value = atomicStampedRef.get(stampHolder);
System.out.println("Value: " + value + ", Stamp: " + stampHolder[0]);
atomicStampedRef.compareAndSet(100, 200, stampHolder[0], stampHolder[0] + 1);
-
时间戳机制:
- 使用时间戳代替版本号,确保每次更新的时间戳唯一。
总结
-
CAS 是什么:
- CAS 是一种硬件级别的原子操作,用于实现无锁同步。
- 它通过比较和更新内存中的值来保证线程安全。
-
优点:
- 高效、无锁、线程安全,适合高并发场景。
-
缺点:
- 存在 ABA 问题。
- 在高竞争场景下可能导致自旋开销。
-
应用场景:
- Java 的
Atomic
类、无锁队列、线程池等。
- Java 的
CAS 是现代并发编程的核心技术之一,通过合理使用 CAS,可以显著提升系统的并发性能。
CAS 的自旋
CAS(Compare-And-Swap,比较并交换) 是一种硬件级别的原子操作,用于实现多线程环境下的无锁同步。在 CAS 操作中,如果当前线程的更新失败(即内存中的值与预期值不一致),线程会不断重试,直到更新成功。这种不断重试的行为被称为 CAS 的自旋。
CAS 自旋的工作原理
1. 基本流程
CAS 的自旋逻辑如下:
- 线程读取共享变量的当前值。
- 比较当前值是否等于预期值(
expectedValue
)。 - 如果相等,则将共享变量更新为新值(
newValue
),操作成功,退出自旋。 - 如果不相等,则重新读取共享变量的值,重复上述步骤,直到更新成功。
2. 伪代码
以下是 CAS 自旋的伪代码:
while (true) {
// 获取当前值
int currentValue = memoryAddress.value;
// 比较并尝试更新
if (compareAndSwap(memoryAddress, currentValue, newValue)) {
break; // 更新成功,退出自旋
}
// 更新失败,继续自旋
}
3. 自旋的特点
- 无锁:线程不会阻塞,而是通过不断重试来完成操作。
- 高效:避免了线程上下文切换的开销(如加锁和解锁)。
- 适合低竞争场景:在锁竞争较少的情况下,自旋通常能快速完成操作。
CAS 自旋的优点
-
避免线程阻塞:
- 自旋操作不会导致线程进入阻塞状态,避免了线程上下文切换的开销。
- 适合短时间的锁竞争场景。
-
高性能:
- CAS 自旋依赖硬件级别的原子操作(如
CMPXCHG
指令),性能非常高。
- CAS 自旋依赖硬件级别的原子操作(如
-
无锁同步:
- 自旋是无锁算法的核心,避免了传统锁机制(如
synchronized
或ReentrantLock
)带来的性能瓶颈。
- 自旋是无锁算法的核心,避免了传统锁机制(如
CAS 自旋的缺点
-
高竞争场景下的性能问题:
- 如果多个线程同时竞争同一个资源,自旋可能会导致大量的 CPU 资源浪费,因为线程会不断重试。
-
自旋时间过长:
- 如果共享变量长时间无法更新,线程会一直自旋,可能导致 CPU 占用率过高。
-
无法解决复杂同步问题:
- CAS 自旋只能保证单个变量的原子性,无法直接处理多个变量的同步问题。
-
ABA 问题:
- 自旋无法解决 CAS 的 ABA 问题(即值从
A
变为B
,然后又变回A
),需要额外的机制(如版本号)来解决。
- 自旋无法解决 CAS 的 ABA 问题(即值从
CAS 自旋的实现
1. Java 中的 CAS 自旋
Java 的 Atomic
类(如 AtomicInteger
、AtomicBoolean
)底层依赖 CAS 操作,并通过自旋实现线程安全。
示例:AtomicInteger
的自旋实现
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
-
compareAndSwapInt
:- 这是一个本地方法,调用底层的硬件指令(如
CMPXCHG
)实现 CAS 操作。 - 如果 CAS 操作失败,
compareAndSet
会不断重试,直到成功。
- 这是一个本地方法,调用底层的硬件指令(如
2. 自旋锁的实现
自旋锁是一种基于 CAS 的锁实现,线程在获取锁失败时会自旋等待,而不是进入阻塞状态。
示例:自旋锁的简单实现
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
private final AtomicBoolean lock = new AtomicBoolean(false);
public void lock() {
while (!lock.compareAndSet(false, true)) {
// 自旋等待
}
}
public void unlock() {
lock.set(false);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable task = () -> {
spinLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
Thread.sleep(1000); // 模拟任务
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock");
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
}
输出示例:
Thread-0 acquired the lock
Thread-0 released the lock
Thread-1 acquired the lock
Thread-1 released the lock
CAS 自旋的适用场景
-
低竞争场景:
- 当锁竞争较少时,自旋通常能快速完成操作,避免线程阻塞。
-
短时间的临界区:
- 如果临界区的操作非常短(如简单的计数器递增),自旋的性能优于传统锁。
-
高性能场景:
- 在高性能并发框架中(如
ConcurrentHashMap
、ThreadPoolExecutor
),CAS 自旋被广泛使用。
- 在高性能并发框架中(如
CAS 自旋的优化
-
限制自旋次数:
- 为了避免线程长时间自旋,可以设置最大自旋次数。如果超过次数仍未成功,则线程进入阻塞状态。
- 示例:
public void lock() {
int maxSpin = 1000; // 最大自旋次数
int spins = 0;
while (!lock.compareAndSet(false, true)) {
if (++spins > maxSpin) {
// 超过自旋次数,进入阻塞状态
Thread.yield();
}
}
}
-
自适应自旋:
- 根据锁竞争的历史记录动态调整自旋次数。例如,JVM 的
ReentrantLock
使用了自适应自旋策略。
- 根据锁竞争的历史记录动态调整自旋次数。例如,JVM 的
-
结合其他同步机制:
- 在高竞争场景下,可以结合传统锁或其他同步机制(如
Semaphore
)来减少自旋的开销。
- 在高竞争场景下,可以结合传统锁或其他同步机制(如
CAS 自旋的优缺点对比
特性 | 优点 | 缺点 |
---|---|---|
线程安全性 | 基于硬件级别的 CAS 操作,线程安全。 | 在高竞争场景下可能导致大量 CPU 资源浪费。 |
性能 | 避免线程阻塞和上下文切换,性能高。 | 自旋时间过长可能导致性能下降。 |
适用场景 | 适合低竞争、短时间的临界区操作。 | 不适合高竞争或长时间的临界区操作。 |
复杂性 | 实现简单,依赖 CAS 操作即可。 | 无法直接解决 ABA 问题,需要额外机制(如版本号)。 |
总结
-
CAS 自旋的定义:
- CAS 自旋是指线程在 CAS 操作失败时不断重试,直到操作成功。
-
优点:
- 避免线程阻塞,性能高,适合低竞争场景。
-
缺点:
- 在高竞争场景下可能导致 CPU 资源浪费,自旋时间过长会影响性能。
-
适用场景:
- 低竞争、短时间的临界区操作。
-
优化策略:
- 限制自旋次数、自适应自旋、结合其他同步机制。
CAS 自旋是现代并发编程中非常重要的技术,通过合理使用和优化,可以显著提升系统的并发性能。
Java CAS 自旋实现
Java 的 CAS 自旋实现依赖于底层硬件指令,而这些硬件指令通过操作系统的支持被调用。具体来说,Java 的 CAS 自旋是通过 CPU 提供的原子指令(如 x86 架构的 CMPXCHG
或 ARM 架构的 LDREX/STREX
)实现的,而这些指令由操作系统和 JVM 的本地代码(Native Code)桥接到 Java 层。
Java CAS 自旋的实现流程
-
Java 层的自旋逻辑:
- Java 的 CAS 自旋逻辑是通过
Unsafe
类或Atomic
类(如AtomicInteger
)实现的。 - 当 CAS 操作失败时,线程会进入自旋状态,不断重试,直到 CAS 操作成功。
- Java 的 CAS 自旋逻辑是通过
-
JVM 层的 CAS 实现:
- JVM 使用
Unsafe
类的compareAndSwapXXX
方法来实现 CAS 操作。 - 这些方法是
native
方法,通过 JNI 调用本地代码。
- JVM 使用
-
本地代码层:
- 本地代码(通常是 C/C++ 编写的)会调用 CPU 提供的原子指令(如
CMPXCHG
或LDREX/STREX
)。 - 这些指令直接在硬件层面执行,保证了比较和交换操作的原子性。
- 本地代码(通常是 C/C++ 编写的)会调用 CPU 提供的原子指令(如
-
硬件层的原子指令:
- CPU 提供的原子指令(如
CMPXCHG
)能够在一个时钟周期内完成比较和交换操作,避免了中断或其他线程的干扰。 - 如果 CAS 操作失败,Java 层会进入自旋状态,继续调用底层指令重试。
- CPU 提供的原子指令(如
自旋的核心:硬件指令的支持
1. x86 架构:CMPXCHG
指令
- 作用:比较内存中的值和寄存器中的值,如果相等,则更新内存中的值。
- 原子性:通过缓存一致性协议(如 MESI 协议)或总线锁保证操作的原子性。
2. ARM 架构:LDREX
和 STREX
指令
- 作用:
LDREX
加载内存值并标记为独占访问,STREX
尝试更新内存值,如果期间有其他线程修改了该值,则更新失败。 - 原子性:通过独占访问标记和缓存一致性协议实现。
3. 硬件级别的原子性保证
- 总线锁:在单核 CPU 中,CAS 操作通过锁定总线实现原子性。
- 缓存一致性协议:在多核 CPU 中,CAS 操作通过锁定缓存行实现原子性。
Java CAS 自旋的实现示例
以下是一个基于 AtomicInteger
的 CAS 自旋示例:
import java.util.concurrent.atomic.AtomicInteger;
public class CASSpinExample {
private static final AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (!counter.compareAndSet(0, 1)) {
// 自旋等待
}
System.out.println("Thread 1 updated counter to 1");
});
Thread thread2 = new Thread(() -> {
while (!counter.compareAndSet(1, 2)) {
// 自旋等待
}
System.out.println("Thread 2 updated counter to 2");
});
thread1.start();
thread2.start();
}
}
执行流程:
counter.compareAndSet(0, 1)
调用Unsafe.compareAndSwapInt
方法。- JVM 通过 JNI 调用本地代码,执行硬件指令(如
CMPXCHG
)。 - 如果 CAS 操作失败,线程会进入自旋状态,继续重试。
自旋的优缺点
优点
-
避免线程阻塞:
- 自旋不会导致线程进入阻塞状态,避免了线程上下文切换的开销。
-
高性能:
- 在低竞争场景下,自旋通常能快速完成操作,性能优于传统锁。
缺点
-
高竞争场景下的性能问题:
- 如果多个线程同时竞争同一个资源,自旋可能导致大量的 CPU 资源浪费。
-
自旋时间过长:
- 如果共享变量长时间无法更新,线程会一直自旋,可能导致 CPU 占用率过高。
自旋的优化策略
-
限制自旋次数:
- 设置最大自旋次数,如果超过次数仍未成功,则线程进入阻塞状态。
- 示例:
int maxSpin = 1000;
int spins = 0;
while (!counter.compareAndSet(0, 1)) {
if (++spins > maxSpin) {
Thread.yield(); // 让出 CPU
}
}
-
自适应自旋:
- 根据锁竞争的历史记录动态调整自旋次数。
- JVM 的
ReentrantLock
使用了自适应自旋策略。
-
结合其他同步机制:
- 在高竞争场景下,可以结合传统锁或其他同步机制(如
Semaphore
)来减少自旋的开销。
- 在高竞争场景下,可以结合传统锁或其他同步机制(如
总结
-
Java CAS 自旋的实现依赖于底层硬件指令:
- Java 层通过
Unsafe
类调用 CAS 操作。 - JVM 使用 JNI 调用本地代码,最终执行硬件指令(如
CMPXCHG
或LDREX/STREX
)。
- Java 层通过
-
自旋的核心是硬件指令的原子性:
- 硬件指令通过总线锁、缓存一致性协议或独占访问标记实现原子性。
-
自旋的优缺点:
- 自旋适合低竞争场景,但在高竞争场景下可能导致性能问题。
通过硬件指令的支持,Java 的 CAS 自旋能够高效地实现无锁同步,是现代并发编程的重要基础。
Unsafe.compareAndSetInt
方法本身没有自旋等待的操作
Unsafe.compareAndSetInt
方法本身没有自旋等待的操作,它只是一个单次的 CAS(Compare-And-Swap)操作。如果调用 compareAndSetInt
时 CAS 操作失败,它会直接返回 false
,而不会自动重试或自旋。
因此,自旋等待的逻辑需要由调用者在 Java 层显式实现,例如通过 while
循环不断调用 compareAndSetInt
来实现自旋。
1. compareAndSetInt
的行为
方法定义
compareAndSetInt
是 Unsafe
类中的一个方法,定义如下:
public final native boolean compareAndSetInt(Object obj, long offset, int expected, int newValue);
行为
-
单次 CAS 操作:
- 它会尝试将对象
obj
的某个字段(通过offset
指定)从expected
值更新为newValue
。 - 如果内存中的值等于
expected
,更新成功,返回true
。 - 如果内存中的值不等于
expected
,更新失败,返回false
。
- 它会尝试将对象
-
无自旋逻辑:
- 如果更新失败,
compareAndSetInt
不会自动重试或等待,而是直接返回。
- 如果更新失败,
2. 自旋等待的实现
为什么需要自旋?
- 如果 CAS 操作失败,通常是因为其他线程修改了目标值。
- 在某些场景下(如高并发环境),我们希望线程不断重试,直到 CAS 操作成功。这种重试逻辑需要通过显式的自旋等待实现。
如何实现自旋等待?
自旋等待的逻辑需要在 Java 层通过循环调用 compareAndSetInt
来实现。例如:
while (!unsafe.compareAndSetInt(obj, offset, expected, newValue)) {
// 自旋等待,直到 CAS 操作成功
}
完整示例
以下是一个基于 Unsafe
类的自旋等待示例:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class CASExample {
private static final Unsafe unsafe;
private static final long valueOffset;
private volatile int value = 0;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
valueOffset = unsafe.objectFieldOffset(CASExample.class.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
public void increment() {
int expected;
int newValue;
do {
expected = unsafe.getIntVolatile(this, valueOffset); // 获取当前值
newValue = expected + 1; // 计算新值
} while (!unsafe.compareAndSetInt(this, valueOffset, expected, newValue)); // 自旋等待
}
public int getValue() {
return value;
}
public static void main(String[] args) {
CASExample example = new CASExample();
// 启动多个线程并发执行
for (int i = 0; i < 10; i++) {
new Thread(example::increment).start();
}
try {
Thread.sleep(1000); // 等待线程执行完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final value: " + example.getValue());
}
}
输出示例
Final value: 10
3. 为什么 compareAndSetInt
不自带自旋?
1. 灵活性
- 如果
compareAndSetInt
内部实现了自旋等待,调用者就无法控制自旋的逻辑(如自旋次数、超时策略等)。 - 通过将自旋逻辑交给调用者,提供了更大的灵活性。
2. 性能考虑
- 自旋等待可能会导致 CPU 资源的浪费,尤其是在高竞争场景下。
- 有些场景下,开发者可能希望在 CAS 失败后直接放弃操作,而不是自旋等待。
3. 适用场景
- 自旋适合短时间的锁竞争场景。如果竞争时间较长,自旋会浪费大量 CPU 资源,通常需要结合其他机制(如
Thread.yield
或阻塞队列)来避免性能问题。
4. 自旋等待的优化策略
1. 限制自旋次数
- 设置最大自旋次数,避免线程长时间占用 CPU。
- 示例:
int maxSpin = 1000;
int spins = 0;
while (!unsafe.compareAndSetInt(obj, offset, expected, newValue)) {
if (++spins > maxSpin) {
Thread.yield(); // 让出 CPU
}
}
2. 自适应自旋
- 根据锁竞争的历史记录动态调整自旋次数。
- 示例:
int adaptiveSpin = calculateAdaptiveSpin(); // 动态计算自旋次数
while (!unsafe.compareAndSetInt(obj, offset, expected, newValue) && adaptiveSpin-- > 0) {
// 自旋等待
}
3. 结合阻塞机制
- 在高竞争场景下,可以结合阻塞机制(如
LockSupport.park
)来减少 CPU 资源浪费。 - 示例:
while (!unsafe.compareAndSetInt(obj, offset, expected, newValue)) {
LockSupport.parkNanos(100); // 等待一段时间后重试
}
5. 总结
-
compareAndSetInt
本身没有自旋等待逻辑:- 它是一个单次 CAS 操作,失败后直接返回
false
。
- 它是一个单次 CAS 操作,失败后直接返回
-
自旋等待需要调用者显式实现:
- 通过
while
循环不断调用compareAndSetInt
来实现自旋等待。
- 通过
-
为什么不内置自旋?
- 提供灵活性,允许调用者根据具体场景控制自旋逻辑。
- 避免在高竞争场景下浪费 CPU 资源。
-
优化策略:
- 限制自旋次数、自适应自旋、结合阻塞机制等。
通过这种设计,Java 的 CAS 操作既能满足高性能场景的需求,又能提供足够的灵活性,让开发者根据具体场景选择合适的实现方式。
CAS 的操作系统指令
CAS(Compare-And-Swap,比较并交换)操作依赖于 CPU 提供的硬件指令来实现原子性。这些指令通过硬件机制确保比较和更新操作是不可分割的(原子的),即使在多核、多线程环境下也能保证数据一致性。
以下是常见 CPU 架构中支持 CAS 操作的指令及其工作原理:
1. x86 架构:CMPXCHG
指令
指令简介
CMPXCHG
(Compare and Exchange) 是 x86 架构中用于实现 CAS 的指令。- 它比较寄存器中的值与内存中的值,如果相等,则将寄存器中的新值写入内存;否则,不更新内存,并将内存中的值加载到寄存器中。
工作流程
- 比较内存地址中的值(
memory
)与寄存器中的预期值(EAX
)。 - 如果相等,则将寄存器中的新值写入内存。
- 如果不相等,则将内存中的值加载到寄存器中。
伪代码
CMPXCHG [memory], newValue
等价于以下伪代码:
if (memory == EAX) {
memory = newValue;
} else {
EAX = memory;
}
原子性保证
-
总线锁(Bus Locking) :
- 在单核 CPU 中,
LOCK
前缀可以锁定总线,确保指令执行期间其他处理器无法访问内存。
- 在单核 CPU 中,
-
缓存一致性协议(MESI) :
- 在多核 CPU 中,
CMPXCHG
使用缓存一致性协议(如 MESI 协议)来确保内存操作的原子性。 - 当一个核心执行
CMPXCHG
时,会锁定目标内存地址所在的缓存行,其他核心无法同时修改该地址。
- 在多核 CPU 中,
示例
以下是使用 CMPXCHG
指令实现 CAS 的示例:
mov eax, expectedValue ; 将预期值加载到 EAX 寄存器
mov ebx, newValue ; 将新值加载到 EBX 寄存器
lock cmpxchg [memory], ebx ; 比较并交换内存中的值
2. ARM 架构:LDREX
和 STREX
指令
指令简介
- ARM 架构中没有直接的 CAS 指令,但通过
LDREX
(Load Exclusive) 和STREX
(Store Exclusive) 指令组合实现类似的功能。 - 这些指令用于实现乐观锁,即先加载值,再尝试更新值,如果期间值被其他线程修改,则更新失败。
工作流程
-
LDREX
:- 加载内存地址中的值到寄存器,并标记该地址为“独占访问”。
-
STREX
:- 尝试将新值写入内存地址。
- 如果期间该地址被其他线程修改,则写入失败,并返回一个失败标志。
伪代码
LDREX R1, [memory] ; 加载内存中的值到 R1
CMP R1, expectedValue ; 比较 R1 和预期值
BNE fail ; 如果不相等,跳转到失败分支
STREX R2, newValue, [memory] ; 尝试将新值写入内存
CMP R2, #0 ; 检查 STREX 是否成功
BNE retry ; 如果失败,重新尝试
原子性保证
-
独占访问标记(Exclusive Access Tag) :
- ARM 处理器在执行
LDREX
时,会标记目标内存地址为“独占访问”。 - 如果在
LDREX
和STREX
之间,其他线程修改了该地址,STREX
会检测到冲突并返回失败。
- ARM 处理器在执行
-
缓存一致性协议:
- ARM 处理器使用缓存一致性协议(如 MOESI 协议)来确保内存操作的原子性。
示例
以下是 ARM 架构中使用 LDREX
和 STREX
实现 CAS 的示例:
retry:
LDREX R1, [memory] ; 加载内存中的值到 R1
CMP R1, expectedValue ; 比较 R1 和预期值
BNE fail ; 如果不相等,跳转到失败分支
STREX R2, newValue, [memory] ; 尝试将新值写入内存
CMP R2, #0 ; 检查 STREX 是否成功
BNE retry ; 如果失败,重新尝试
fail:
3. RISC-V 架构:LR/SC
指令
指令简介
- RISC-V 架构使用
LR
(Load-Reserved) 和SC
(Store-Conditional) 指令组合实现 CAS 操作。 - 类似于 ARM 的
LDREX
和STREX
,LR
和SC
也基于乐观锁的思想。
工作流程
-
LR
:- 加载内存地址中的值到寄存器,并标记该地址为“保留”。
-
SC
:- 尝试将新值写入内存地址。
- 如果期间该地址被其他线程修改,则写入失败,并返回失败标志。
伪代码
LR R1, [memory] ; 加载内存中的值到 R1
CMP R1, expectedValue ; 比较 R1 和预期值
BNE fail ; 如果不相等,跳转到失败分支
SC R2, newValue, [memory] ; 尝试将新值写入内存
BEQ success ; 如果成功,跳转到成功分支
B retry ; 如果失败,重新尝试
原子性保证
-
保留标记(Reservation Tag) :
- 在执行
LR
时,处理器会标记目标内存地址为“保留”。 - 如果在
LR
和SC
之间,其他线程修改了该地址,SC
会检测到冲突并返回失败。
- 在执行
-
缓存一致性协议:
- RISC-V 处理器使用缓存一致性协议来确保内存操作的原子性。
4. 原子性保证的硬件机制
1. 总线锁(Bus Locking)
- 在单核处理器中,CAS 操作通过锁定总线来实现原子性。
- 当一个处理器执行 CAS 操作时,会锁定目标内存地址所在的总线,其他处理器无法访问该地址。
2. 缓存一致性协议
- 在多核处理器中,CAS 操作通过缓存一致性协议(如 MESI、MOESI)来实现原子性。
- 当一个核心执行 CAS 操作时,会锁定目标内存地址所在的缓存行,其他核心无法同时修改该地址。
3. 独占访问标记
- ARM 和 RISC-V 架构通过独占访问标记(Exclusive Access Tag 或 Reservation Tag)来实现原子性。
- 在加载和存储之间,如果其他线程修改了目标地址,存储操作会失败。
总结
架构 | 指令 | 原子性机制 | 特点 |
---|---|---|---|
x86 | CMPXCHG | 总线锁、缓存一致性协议(MESI) | 直接支持 CAS 操作,性能高,广泛应用于多核处理器。 |
ARM | LDREX 和 STREX | 独占访问标记、缓存一致性协议 | 基于乐观锁的实现,适合低功耗设备和嵌入式系统。 |
RISC-V | LR 和 SC | 保留标记、缓存一致性协议 | 类似 ARM 的实现,支持灵活的原子操作。 |
通过这些硬件指令,CAS 操作能够在多线程环境下高效地实现原子性,避免了传统锁机制的开销,是现代并发编程的核心技术之一。
Java 的 Unsafe 类的 compareAndSwapInt 方法,是怎么访问到操作系统指令的?
在 Java 中,Unsafe
类的 compareAndSwapInt
方法是通过 JNI(Java Native Interface) 调用底层的本地代码(Native Code)实现的,而这些本地代码最终会调用操作系统提供的硬件指令(如 x86 架构的 CMPXCHG
指令或 ARM 架构的 LDREX/STREX
指令)。以下是详细的工作原理和流程:
1. Unsafe
类简介
Unsafe
类 是 Java 中一个位于sun.misc
包下的类,提供了一些底层操作的方法,比如直接操作内存、线程调度、CAS 操作等。- 它是 Java 中实现高性能并发工具(如
AtomicInteger
、ConcurrentHashMap
)的核心工具。 compareAndSwapInt
方法 是Unsafe
类中用于实现 CAS 操作的一个方法。
2. compareAndSwapInt
方法的定义
在 Unsafe
类中,compareAndSwapInt
方法的定义如下:
public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int newValue);
参数说明:
obj
:要操作的对象。offset
:对象中字段的内存偏移量(通过Unsafe.objectFieldOffset
获取)。expected
:预期值。newValue
:要更新的新值。
关键点:
native
关键字:表示该方法是一个本地方法,其实现由本地代码(C/C++)提供,而不是用 Java 编写。- 返回值:
true
表示 CAS 操作成功,false
表示失败。
3. compareAndSwapInt
的实现流程
1. Java 层调用 Unsafe.compareAndSwapInt
- 当 Java 程序调用
compareAndSwapInt
方法时,JVM 会通过 JNI 跳转到本地代码的实现。
2. JNI 调用本地代码
Unsafe
类的本地方法由 JVM 的 C/C++ 实现提供。- 在 OpenJDK 中,
compareAndSwapInt
的实现位于unsafe.cpp
文件中。
3. 本地代码调用硬件指令
- 本地代码通过调用操作系统提供的底层 API 或直接使用汇编指令(如
CMPXCHG
)来实现 CAS 操作。 - 这些硬件指令由 CPU 提供,能够保证比较和交换操作的原子性。
4. OpenJDK 中的实现细节
1. Unsafe
类的本地方法注册
- 在 JVM 启动时,
Unsafe
类的本地方法会通过 JNI 注册到 JVM 中。 - 在 OpenJDK 的
unsafe.cpp
文件中,可以看到compareAndSwapInt
的实现。
2. compareAndSwapInt
的核心实现
以下是 OpenJDK 中 compareAndSwapInt
的核心代码(简化版):
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint expected, jint newValue)) {
oop p = JNIHandles::resolve(obj); // 获取对象的内存地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // 计算字段的内存地址
return (jint)(Atomic::cmpxchg(newValue, addr, expected) == expected); // 调用底层 CAS 实现
} UNSAFE_END
3. Atomic::cmpxchg
的实现
Atomic::cmpxchg
是一个封装了硬件指令的函数,用于执行 CAS 操作。- 在 x86 架构中,它会调用
CMPXCHG
指令:
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
jint old_value;
__asm__ volatile ("lock; cmpxchgl %1, %2"
: "=a" (old_value)
: "r" (exchange_value), "m" (*dest), "0" (compare_value)
: "memory");
return old_value;
}
4. 硬件指令的执行
- 在 x86 架构中,
CMPXCHG
指令会比较内存中的值和寄存器中的值,如果相等,则更新内存中的值。 - 在 ARM 架构中,类似的操作由
LDREX
和STREX
指令完成。
5. 硬件级别的原子性保证
1. 总线锁(Bus Locking)
- 在单核 CPU 中,CAS 操作通过锁定总线来实现原子性。
- 当一个处理器执行 CAS 操作时,会锁定目标内存地址所在的总线,其他处理器无法访问该地址。
2. 缓存一致性协议
- 在多核 CPU 中,CAS 操作通过缓存一致性协议(如 MESI 协议)来实现原子性。
- 当一个核心执行 CAS 操作时,会锁定目标内存地址所在的缓存行,其他核心无法同时修改该地址。
3. 指令级别的原子性
- 硬件指令(如
CMPXCHG
或LDREX/STREX
)由 CPU 提供,能够保证比较和交换操作在一个时钟周期内完成,避免了中断或其他线程的干扰。
6. 总结:从 Java 到硬件的完整流程
-
Java 层:
- Java 程序调用
Unsafe.compareAndSwapInt
方法。
- Java 程序调用
-
JNI 层:
- JVM 通过 JNI 跳转到本地代码(C/C++)的实现。
-
本地代码层:
- 本地代码调用
Atomic::cmpxchg
函数,封装了硬件指令。
- 本地代码调用
-
硬件层:
- CPU 执行硬件指令(如
CMPXCHG
或LDREX/STREX
),保证 CAS 操作的原子性。
- CPU 执行硬件指令(如
7. 为什么 Unsafe
能直接调用硬件指令?
-
JNI 的作用:
Unsafe
类通过 JNI 调用本地代码,而本地代码可以直接使用汇编指令或操作系统提供的 API。- 这种机制使得 Java 能够突破 JVM 的限制,直接访问底层硬件。
-
硬件指令的支持:
- CPU 提供了专门的指令(如
CMPXCHG
)来实现原子操作,操作系统和编译器会封装这些指令供开发者使用。
- CPU 提供了专门的指令(如
-
JVM 的优化:
- JVM 对
Unsafe
类的调用进行了高度优化,确保其性能接近于直接调用硬件指令。
- JVM 对
8. 关键点总结
-
Unsafe.compareAndSwapInt
的实现:- 通过 JNI 调用本地代码,最终使用硬件指令(如
CMPXCHG
或LDREX/STREX
)实现 CAS 操作。
- 通过 JNI 调用本地代码,最终使用硬件指令(如
-
硬件级别的原子性:
- 硬件指令通过总线锁、缓存一致性协议和指令级别的原子性保证 CAS 操作的正确性。
-
性能优势:
- 这种机制避免了传统锁的开销,是实现高性能并发工具的基础。
通过这种设计,Java 的 Unsafe
类能够高效地利用底层硬件的能力,为并发编程提供强大的支持。