CAS 存在三个经典问题,ABA 问题、自旋开销大、只能操作一个变量等.
ABA 问题指的是,一个值原来是 A,后来被改为 B,再后来又被改回 A,这时 CAS 会误认为这个值没有发生变化。
线程 1:CAS(A → B),修改变量 A → B
线程 2:CAS(B → A),变量又变回 A
线程 3:CAS(A → C),CAS 成功,但实际数据已被修改过!
可以使用版本号/时间戳的方式来解决 ABA 问题。
比如说,每次变量更新时,不仅更新变量的值,还更新一个版本号。CAS 操作时,不仅比较变量的值,还比较版本号。
class OptimisticLockExample {
private int version;
private int value;
public synchronized boolean updateValue(int newValue, int currentVersion) {
if (this.version == currentVersion) {
this.value = newValue;
this.version++;
return true;
}
return false;
}
}
Java 的 AtomicStampedReference 就增加了版本号,它会同时检查引用值和 stamp 是否都相等。
示例:
class ABAFix {
private static AtomicStampedReference<String> ref = new AtomicStampedReference<>("100", 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = ref.getStamp();
ref.compareAndSet("100", "200", stamp, stamp + 1);
ref.compareAndSet("200", "100", ref.getStamp(), ref.getStamp() + 1);
}).start();
new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) {}
int stamp = ref.getStamp();
System.out.println("CAS 结果:" + ref.compareAndSet("100", "300", stamp, stamp + 1));
}).start();
}
}
自旋开销大怎么办?
CAS 失败时会不断自旋重试,如果一直不成功,会给 CPU 带来非常大的执行开销。
可以加一个自旋次数的限制,超过一定次数,就切换到 synchronized 挂起线程。
int MAX_RETRIES = 10;
int retries = 0;
while (!atomicInt.compareAndSet(expect, update)) {
retries++;
if (retries > MAX_RETRIES) {
synchronized (this) { // 超过次数,使用 synchronized 处理
if (atomicInt.get() == expect) {
atomicInt.set(update);
}
}
break;
}
}
多个变量同时更新问题?
可以将多个变量封装为一个对象,使用 AtomicReference 进行 CAS 更新。
class Account {
static class Balance {
final int money;
final int points;
Balance(int money, int points) {
this.money = money;
this.points = points;
}
}
private AtomicReference<Balance> balance = new AtomicReference<>(new Balance(100, 10));
public void update(int newMoney, int newPoints) {
Balance oldBalance, newBalance;
do {
oldBalance = balance.get();
newBalance = new Balance(newMoney, newPoints);
} while (!balance.compareAndSet(oldBalance, newBalance));
}
}