基础巩固篇 —— 对CAS的理解
一、CAS是什么
比较后交换,为了保证原子性而进行的比较和交换。
二、CAS的使用
前面说到volatile关键字是不保证原子性的,为了满足轻量级的JMM原则,可以通过volatile + CAS实现轻量级的JMM原则(保证数据可见性、保证原子性、禁止指令重排以保证有序性原则)。例如:
public class VolatileDemo {
// 定义volatile修饰的原子包装类
public volatile AtomicInteger number = new AtomicInteger();
// 实现原子整型包装类的自增i++
public void atomicDemo() {
number.getAndIncrement();
}
}
/**
* 测试20个线程各执行1000次自增后结果
*/
@Test
public void atomicDemo() {
VolatileDemo volatileDemo = new VolatileDemo();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
volatileDemo.atomicDemo();
}
}, String.valueOf(i)).start();
}
// 等自建的所有线程执行完成后再执行以下代码,因为程序默认存在main线程和GC垃圾回收线程, 故>2
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(volatileDemo.number.get());
}
三、CAS原理
在AtomicInteger原子整型包装类方法中,使用的关键class是Unsafe,这个类中的方法是由native修饰的,这是jdk与计算机系统之间数据操作的约定后门,类比Thread中的start方法的底层。这些类存在于jdk本身自带的rt.jar中。
四、CAS缺点
在AtomicInteger中存在方法compareAndSet
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
参数expect是第一次取出的数值(线程从主内存copy到自己的工作内存中),update是在进行数据操作后的数值。当主内存中的数值与expect相同,则将主内存中的数值更新为update数值,并通知其他线程保证可见性,如果不同则不进行更新。这样只关注取出和结果比对的过程,有一个明显的缺陷,那就是ABA问题,ABA问题就是取出时是A,中间不管被其他线程更新多少次,只要在当前线程结果比对之前再变为A,那么就算比对成功,当前线程就还会将其数值认为一直没有变化,对其进行更新。
五、CAS实例
除了AtomicInteger之外,jdk还提供了不同类型的原子型包装类
// 整型原子包装类,默认为0
AtomicInteger atomicInteger = new AtomicInteger();
// 布尔原子包装类,默认返回false
AtomicBoolean atomicBoolean = new AtomicBoolean();
// 自定义类原子包装
AtomicReference<Object> atomicReference = new AtomicReference<>();
AtomicLong atomicLong = new AtomicLong();
// 带有版本号的自定义原子包装类,初始化需要提供版本号
AtomicStampedReference<Object> atomicStampedReference = new AtomicStampedReference<>(null, 1);
六、ABA问题的解决
使用AtomicStampedReference自定义版本原子包装类可以解决ABA问题。原理是,在存到主内存时定义一个stamped版本号,copy值到线程工作内存时连带着版本号,而主内存中的值每更新一次,版本号也跟着自增。当前线程操作完值后进行CAS(比较并交换),此时需要比较值和版本号是否都一致,这样就避免了中间过程中值更新问题。