前言:更多 CAS 相关文章:【JAVA并发】CAS(第一弹)
一、CAS存在的问题:
1、ABA问题
上文说到,CAS在更新值的时候,会判断要修改的A 是否和预期值相同,如果相同则更新新值。但会存在一种情况:线程 t2 在 t1 写回前,已经读取 A,修改成了 B,又修改成了 A,再写回时,满足要修改的值和预期值相同,更新成功。整个过程 t1 是无法知道的, t1 在更新时也能成功;而之后 t2 如果还想用 A,就读取不到了,因为已经是 t1 修改后的 B,这就造成了数据不一致的问题。
产生ABA问题的代码示例:
public class ABAExample {
public static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) {
Thread t2 = new Thread(() -> {
atomicInteger.compareAndSet(100,101);
System.out.println(Thread.currentThread().getName() + "当前的值为:" + atomicInteger.get());
atomicInteger.compareAndSet(101,100);
System.out.println(Thread.currentThread().getName() + "当前的值为:" + atomicInteger.get());
t1.start();
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.compareAndSet(100, 200);
System.out.println(Thread.currentThread().getName() + "当前的值为:" + atomicInteger.get());
});
t2.start();
}
}
日志打印:
Thread-0当前的值为:101
Thread-0当前的值为:100
Thread-1当前的值为:200
目前在 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决ABA问题。这个类的compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该版本的值设置为给定的更新值。
解决方法实例:
public class ABASolve {
//引入AtomicStampedReference来解决ABA问题
public static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "当前的值为:" + atomicStampedReference.getReference() + ",当前的版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "当前的值为:" + atomicStampedReference.getReference() + ",当前的版本号:" + atomicStampedReference.getStamp());
});
t2.start();
Thread t1 = new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,200,stamp,stamp + 1);
System.out.println(Thread.currentThread().getName() + "当前的值为:" + atomicStampedReference.getReference() + ",当前的版本号:" + atomicStampedReference.getStamp());
});
t1.start();
}
}
日志打印:
Thread-0当前的值为:101,当前的版本号:2
Thread-0当前的值为:100,当前的版本号:3
Thread-1当前的值为:100,当前的版本号:3
可以看到线程 t1 最后是更新失败的,虽然要修改的值与预期值相同,但版本号不同。
2、CPU开销:
当线程判断要修改的值和预期值不同时,会循环比较,直到相等为止,造成CPU开销。
3、只能保证一个共享变量的原子操作:
对于多个共享变量可以使用可重入锁,或者封装到一个对象内,使用 AtomicReference 类来保证引用对象之间的原子性。
本文含有隐藏内容,请 开通VIP 后查看