戳蓝色字“码之初”关注,每天都进步!
来源:https://www.zhangjianbing.com/archives/54/
CAS (compare and swap),即:比较然后交换。
CAS 的原理
三个运算符:一个内存地址 V,一个期望值 A,一个新值 B。
基本思路:如果地址 V 上的值和期望值 A 相等,返回 true,并给地址 V 赋上新值 B,如果不是,返回 false,不做任何操作。
循环 (死循环,或者叫自旋) 里不断的进行 CAS 操作。
现代处理器都支持 CAS 的指令,循环这个指令,直到成功为止。
CAS 所带来的问题
1.ABA 问题。
所谓的 ABA 问题就是假设某个内存地址上有一个数值 A,但一个线程过来后把它变成了 B,然后又变回了 A,另一个线程过来后,发现内存地址上的值和期望的值一样,故 CAS 成功了,其实,内存地址上的值发生了变化,这种问题可以用加版本号的方式来解决。下面代码会演示。
2. 系统开销问题。
当一个 CAS 操作永远不成功,它就会一直自旋,系统开销巨大,遇到这种情况,我们只能使用 syn 锁或者其他锁的方式来替代 CAS 操作了。
3. 只能保证一个共享变量的原子操作。
就是只能够保证一个共享变量,如果想保证多个变量的话,可以将这些变量放入一个引用变量中,atomic 为我们提供了操纵引用变量的类,叫 AtomicReference
相关原子操作类
基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
数组类:AtomicIntegerArray,AtomicLongArray
引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
AtomicInteger 基本类型
public class Test {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
// 类似"i++"
System.out.println(ai.getAndIncrement());
// 类似"++i"
System.out.println(ai.incrementAndGet());
System.out.println(ai.get());
}
}
打印结果:
10
12
12
AtomicReference 引用类型
public class Test {
static AtomicReference<User> atomicReference = new AtomicReference();
public static void main(String[] args) {
User user = new User("Ryan1", 18); // 要修改的实例
// 用之包装一下实体类对象
atomicReference.set(user);
// 新对象
User newUser = new User("Ryan2", 20);
// CAS操作
boolean flag = atomicReference.compareAndSet(user, newUser); // 要变化的实例
System.out.println(flag);
// 打印包装类中的对象
System.out.println(atomicReference.get().getName());
System.out.println(atomicReference.get().getAge());
System.out.println("========================");
// 打印原对象
System.out.println(user.getName());
System.out.println(user.getAge());
}
// 定义一个实体类
static class User {
private String name;
private int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
运行结果:
true
Ryan2
20
========================
Ryan1
18
分析:
通过 compareAndSet 方法进行 CAS 操作,可见被包装过的 user 对象本值是不会发生变化的,改变的只是包装的 user 对象,他们两个在被包装的时候就已经不同了。
AtomicReference 存在的问题
public class Test01 {
// 声明引用值为0
static AtomicReference<Integer> atomicReference = new AtomicReference(0);
public static void main(String[] args) throws InterruptedException {
// 引用
final Integer reference = atomicReference.get();
// 原引用
System.out.println("reference初始值:" + reference);
// 新起一个线程用来首次更改
Thread t1 = new Thread(new Runnable() {
public void run() {
Integer reference = atomicReference.get();
System.out.println(reference + "------"
+ atomicReference.compareAndSet(reference, reference + 10));
}
});
// 改回原值
Thread t2 = new Thread(new Runnable() {
public void run() {
Integer reference = atomicReference.get();
System.out.println(reference + "------"
+ atomicReference.compareAndSet(reference, reference - 10));
}
});
// 再做CAS
Thread t3 = new Thread(new Runnable() {
public void run() {
Integer reference = atomicReference.get();
System.out.println(reference + "------"
+ atomicReference.compareAndSet(reference, reference + 10));
}
});
// t1,t2,t3以此执行
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
System.out.println(atomicReference.get());
}
}
运行结果:
reference初始值:0
0------true
10------true
0------true
10
以上代码可以举个例子:
你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是 ABA 问题。上面代码就只关注结果没变就可以修改成功,不关注过程。
AtomicStampedReference 解决 ABA 问题
public class AtomicStampedReferenceTest02 {
// 声明引用值为0,版本号为0
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(0, 0);
public static void main(String[] args) throws InterruptedException {
// 原始版本号
final Integer initialStamp = atomicStampedReference.getStamp();
// 原始引用
final Integer initialReference = atomicStampedReference.getReference();
// 原引用和原版本号
System.out.println("reference初始值: " + initialReference + "版本号初始值: " + initialStamp);
// 新起一个线程用来首次更改
Thread t1 = new Thread(new Runnable() {
public void run() {
System.out.println("原始值:"+initialReference + " ====== 原始版本号:" + initialStamp + " ====== 首次修改结果:"
+ atomicStampedReference.compareAndSet(initialReference, initialReference + 10, initialStamp, initialStamp + 1));
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
Integer stamp = atomicStampedReference.getStamp();
Integer reference = atomicStampedReference.getReference();
System.out.println("当前引用:"+reference + " ====== 当前版本号:" + stamp + " ====== 第二次修改结果:"
+ atomicStampedReference.compareAndSet(reference, reference - 10, stamp, stamp + 1));
}
});
Thread t3 = new Thread(new Runnable() {
public void run() {
// 取到当前引用值和版本号,但是修改的时候使用最开始的版本号
Integer stamp = atomicStampedReference.getStamp();
Integer reference = atomicStampedReference.getReference();
System.out.println("当前引用:"+reference + " ====== 当前版本号:" + stamp + " ====== 第三次修改结果:"
+ atomicStampedReference.compareAndSet(initialReference, reference + 10, initialStamp, stamp + 1));
}
});
// t1,t2,t3顺序执行
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
System.out.println("当前的引用:" + atomicStampedReference.getReference());
System.out.println("当前的版本号:" + atomicStampedReference.getStamp());
}
}
运行结果:
reference初始值: 0版本号初始值: 0
原始值:0 ====== 原始版本号:0 ====== 首次修改结果:true
当前引用:10 ====== 当前版本号:1 ====== 第二次修改结果:true
当前引用:0 ====== 当前版本号:2 ====== 第三次修改结果:false
当前的引用:0
当前的版本号:2
分析:
采用 AtomicStampedReference 来解决 ABA 问题,t1 线程修改了初始值,并将版本号加 1,t2 线程将值修改回了初始值,但是版本号加 1,t3 线程想要修改初始值,虽然当前值跟初始值相等,但是版本号还是用的原来的,故数据修改失败,解决了 ABA 问题。
公众号:码之初
Java技术分享、面试资料大全
程序猿生活记录,长按关注=进步!

春风又绿江南岸,客观何时点"在看"
本文分享自微信公众号 - 码之初(ma_zhichu)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/2414709/blog/4704834