在上文中,我们提到了原子类的底层重要基于CAS来实现,CAS的全称是:Compare and Swap,翻译过来就是:比较并替换。
CAS是实现并发算法时常用的一种技能,它包含三个操作数:内存位置、预期原值及新值。在实行CAS操作的时候,会将内存位置的值与预期原值比较,如果一致,会将该位置的值更新为新值;否则,不做任何操作。
我们照旧以上文介绍的AtomicInteger为例,部分源码内容如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 使用 Unsafe.compareAndSwapInt 方法进行 CAS 操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
当并发数量比较低的时候,采用CAS这种方式可以实现更快的实行效率;当并发数量比较高的时候,由于存在循环比较与替换的逻辑,如果长时间循环,可能会更加斲丧 CPU 资源,此时采用synchronized或Lock来实现线程同步,可能会更有优势。
四、ABA题目
从上文的分析中,我们知道 CAS 在操作的时候会检查预期原值是否发生变化,当预期原值没有发生变化才会更新值。
在现实业务中,可能会出现这么一个征象:线程 t1 正尝试将共享变量的值 A 举行修改,但还没修改;此时另一个线程 t2 获取到 CPU 时间片,将共享变量的值 A 修改成 B,然后又修改为 A,此时线程 t1 检查发现共享变量的值没有发生变化,就会主动去更新值,导致出现了错误更新,但是现实上原始值在这个过程中发生了好几次变化。这个征象我们称它为 ABA 题目。
ABA 题目的解决思绪就是使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加 1,原来的A-B-A就会变成1A-2B-3A。
在java.util.concurrent.atomic包下提供了AtomicStampedReference类,它支持指定版本号来更新,可以通过它来解决 ABA 题目。
在AtomicStampedReference类的compareAndSet()方法中,会检查当前引用是否即是预期引用,并且当前版本号是否即是预期版本号,如果全部相当,则以原子方式将该引用的值设置为给定的更新值,同时更新版本号。
具体示例如下:
// 初始化一个带版本号的原子操作类,原始值:a,原始版本号:1
AtomicStampedReference<String> reference = new AtomicStampedReference<>("a", 1);