引言
在多线程环境中,数据的一致性和线程安全是编程者需要特别关注的点。锁机制是一种常见的解决方案,但它带来了上下文切换和潜在的死锁问题。为了解决这些问题,CAS(Compare-and-Swap)机制应运而生。本文将介绍CAS的概念、核心知识点,并通过实际代码案例进行讲解。
一、CAS 机制简介
CAS是一种用于实现无锁编程的原子操作,全称为“比较并交换”。
它涉及到三个操作数:内存中的值(V),预期原值(A),和新值(B)。
仅当内存中的当前值与预期原值相等时,才将内存中的值更新为新值。
CAS操作通常用于实现无锁数据结构,如无锁队列、无锁哈希表等。通过CAS操作,我们可以在不使用锁的情况下实现线程安全的并发访问。
二、 CAS工作原理
CAS的工作原理基于处理器提供的原子指令。当多个线程同时访问某个共享变量时,CAS操作可以确保在更新该变量时,其他线程不会看到中间状态的值。
具体来说,CAS操作包含以下三个步骤:
- 读取:读取内存位置V的值。
- 比较:将读取到的值与期望的原值A进行比较。
- 交换:如果读取到的值等于A,则将内存位置V的值设置为新值B;否则不做任何操作。
由于CAS操作是一个原子操作,因此它在多线程环境下是安全的。
CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
以下是一个使用CAS操作的伪代码示例:
function atomicCAS(address V, expected value A, new value B):
// 读取内存位置V的值
oldValue = load(V)
// 比较读取到的值与期望的原值A是否相等
if oldValue == A:
// 如果相等,则将内存位置V的值设置为新值B
if compareAndSet(V, A, B):
return true // 操作成功
return false // 操作失败
三、Java 原子包 java.util.concurrent.atomic(锁自旋)
Java 的 java.util.concurrent.atomic 包提供了大量的基于 CAS 实现的原子类,用于在多线程环境中实现线程安全的变量操作。
AtomicInteger 是 Java 并发包 java.util.concurrent.atomic 中的一个类,它提供了对整数值的原子操作
以下是 AtomicInteger 的核心实现概念:
- Unsafe 类:AtomicInteger 使用 sun.misc.Unsafe 类进行底层的内存访问。这个类提供了对内存的直接操作,包括 CAS 操作。因为 Unsafe 是内部 API,并且不是公共的,所以直接使用它是不推荐的,但 AtomicInteger 和其他 Atomic 类是 Java 标准库的一部分,可以放心使用。
- valueOffset:AtomicInteger 使用一个 long 类型的 valueOffset 来表示 value 字段在对象内存布局中的偏移量。这是因为 Unsafe 类需要知道要操作的字段在内存中的确切位置。
- compareAndSet:这是 AtomicInteger 的核心方法,它使用 CAS 操作来比较当前值是否与预期值相等,如果相等,则更新为新值。该方法接受两个参数:expect(预期值)和 update(新值)。如果当前值等于 expect,则更新为 update 并返回 true;否则返回 false。
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;;) { //CAS自旋,一直尝试,直达成功
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
注意,这里的 unsafe.compareAndSwapInt 就是一个 CAS 操作。它尝试将 this 对象在 valueOffset 偏移量处的 int 值从 expect 更新为 update。如果当前值等于 expect,则更新成功并返回 true;否则不执行任何操作并返回 false。
getAndIncrement采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操作。
其他方法:基于 compareAndSet 方法,AtomicInteger 还提供了其他常用的原子操作,如 incrementAndGet(原子地增加当前值并返回更新后的值)、decrementAndGet(原子地减少当前值并返回更新后的值)等。
四、ABA问题
1. ABA 问题描述
CAS的一个常见问题是ABA问题。当一个值在变化过程中被改回原值时,CAS会认为该值没有变化,这可能导致错误的逻辑判断。
ABA问题主要发生在CAS操作中,当某个线程将某个变量的值从A更改为B,然后又将其改回A时,另一个线程可能会误以为这个值从未被修改过。这是因为CAS操作仅比较和交换了值,而没有考虑到值的变化历史。如果另一个线程在此期间读取了该变量的值(此时为A),并尝试使用CAS将其更改为C,由于当前值仍为A(尽管它已经被其他线程修改过),因此CAS操作会成功,从而导致数据不一致的问题。
2.解决方案:
为了解决ABA问题,可以引入版本号或时间戳等机制来标识数据的变化。
- 版本号机制:
部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。 - 时间戳机制:
与版本号机制类似,但使用时间戳来标识数据的变化。
当线程尝试使用CAS操作修改数据时,除了比较值之外,还需要比较时间戳。
如果时间戳不匹配,则CAS操作失败。
在修改数据时,同时更新时间戳。
时间戳机制可以提供更细粒度的时间信息,但也可能需要更复杂的处理来确保时间戳的唯一性和一致性。
3.实际案例
AtomicStampedReference 通过 引入一个版本号(stamp)来帮助识别在比较和交换过程中值是否已经被其他线程改变过,下面是一个简单的例子:
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAExample {
private static AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(0, 0);
public static void main(String[] args) {
int[] stamp = new int[1];
// 第一次操作,成功更新
asr.compareAndSet(0, 1, stamp[0], stamp[0] + 1);
// 模拟ABA情况,读取当前值和stamp
Integer current = asr.getReference();
int currentStamp = asr.getStamp(stamp);
// 模拟值变化回原值
asr.compareAndSet(current, current, currentStamp, currentStamp + 1);
// 下一次更新,由于使用了stamp,可以避免ABA问题
asr.compareAndSet(current, 2, stamp[0], stamp[0] + 1);
}
}