比较并交换(compare and set)
CAS 翻译成中文即:比较并交换,他是一条 CPU 原语操作(保证读写的原子性),底层基于 c/c++ 实现,直接通过指针操作内存实现。通过使用 CAS 原语能够解决并发更新数据的问题,不用额外加锁去保证线程安全。
CAS 的使用
以下例子,通过 AtomicInteger 类的两个方法阐述 CAS 的使用。
getAndIncrement()
有个面试高频考点,i++
线程安全吗?
实际上在多线程环境下,基于 JMM 模型,每个线程都有各自的本地内存,线程工作时会从主内存拷贝一份自己需要的数据,在值回写主内存时容易引发线程安全问题。
比如两个线程 t1,t2 同时进行 i++
操作,
- t1 与 t2 同时从主内存拷贝了 i=0 的值到本地内存
- t1 率先完成了两次 ++ 操作,此时主内存值为 2。t2 因故挂起未执行,且 t2 的本地内存值为 0。
- t2 被唤醒,执行 1 次 ++ 操作,回写主内存,将主内存的值覆盖为 1,至此引发线程安全问题。
有同学可能要讲,加上 volatile
保证可见性,就不会有上述问题了,那我们继续看看:
首先 i++ 实际上是由四条指令组成的
第一条指令 getfield :取出 n 这个元素
第二条指令 iconst_1 :把常量 1 压入栈
第三条指令 iadd :把 n 和常量 1 进行相加
第四条指令 putfield :把数据刷回主内存
t1 与 t2 即便能读取到最新的值,但盖不住 i++ 操作非原子,上述地三条指令若同时被执行,则同样引发线程安全问题。
volatile 能够保证安全性,但是不能保证原子性,它仅能保证读到最新的数据。
跑代码看看:
@Data public class VolatileTestData { /** * 普通int类型 */ private int number = 0; /** * 原子类int */ private AtomicInteger atomicInteger = new AtomicInteger(); /** * 使用++操作,线程不安全。 * * @return: void * @author openshell * @date: 2022/2/3 10:48 */ public void add() { number++; } /** * 基于CPU原语,CAS原理进行自增,线程安全。 * * @return: void * @author openshell * @date: 2022/2/3 10:48 */ public void atomicAdd() { atomicInteger.getAndIncrement(); } }
@Test public void testAtomicity() { VolatileTestData volatileTestData = new VolatileTestData(); for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { //调用i++方法自增 volatileTestData.add(); //使用AtomicInteger自增(CAS) volatileTestData.atomicAdd(); } }).start(); } //注: //1.Thread.activeCount()用于返回当前线程的线程组中活动线程的数量,返回的值只是一个估计值,因为当此方法遍历内部数据结构时,线程数可能会动态更改。 //2.此处无内部数据结构故无影响 //3.IntelliJ IDEA执行用户代码的时候,实际是通过反射方式去调用,而与此同时会创建一个Monitor Ctrl-Break 用于监控目的,故线程会多一个。 while (Thread.activeCount() > 2) { System.out.println("线程数量:" + Thread.activeCount()); //线程回退到可运行状态,将cpu时间交给同级或者更高优先级的线程 Thread.yield(); } System.out.println("普通i++的执行结果:" + volatileTestData.getNumber()); System.out.println("Atomically increments的结果:" + volatileTestData.getAtomicInteger()); }
执行结果:
线程数量:4 线程数量:4 普通i++的执行结果:18306 Atomically increments的结果:20000
多次运行 i++ 操作均无法满足期望值,原子整型的计算结果总是正确的。
compareAndSet()
运行:
AtomicInteger atomicInteger = new AtomicInteger(5); System.out.println(atomicInteger.compareAndSet(5, 2021) + "\t 当前值为:" + atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t 当前值为:" + atomicInteger.get());
输出:
true 当前值为:2021 false 当前值为:2021
compareAndSet 源码:
若当前值等于期望值,则原子的更新给定的值。
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
该方法调用了 unsafe 类,unsafe 位于 jre 下的 rt.jar,由于该类脱离了 jvm 的管理,直接操作内存,可能带来不可预估的错误,故取名为 unsafe。
CAS 的缺点
尽管 cas 算法保证了数据读写的原子性,但仍存在一些问题。
-
ABA 问题
eg:有三个线程 t1 t2 t3,同时修改变量 i,t1 与 t2 的功能是将 i 改为 B,t3 的功能是将 i 改为 A:- t1 执行成功,i=B,t2 被挂起
- t3 被唤醒,将 i 改为 A
- t2 被唤醒,此时值为 A,t2 仍然执行 i=B 的操作,t2 并不知道第二步的发生,至此产生 ABA 问题
解决办法:CAS 属于乐观锁,可以在使用时增加版本号,解决以上问题。
代码示例:
public class ABAResolveTest { public static void main(String[] args) { testStamp(); } private static void testStamp() { AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1); new Thread(()->{ int[] stampHolder = new int[1]; //返回当前相同引用的值和信号量 int value = atomicStampedReference.get(stampHolder); int stamp = stampHolder[0]; System.out.println("thread 1 read value: " + value + ", stamp: " + stamp); // 阻塞1s LockSupport.parkNanos(1000000000L); if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) { System.out.println("thread 1 update from " + value + " to 3"); } else { System.out.println("thread 1 update fail!"); } }).start(); new Thread(()->{ int[] stampHolder = new int[1]; int value = atomicStampedReference.get(stampHolder); int stamp = stampHolder[0]; System.out.println("thread 2 read value: " + value + ", stamp: " + stamp); if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) { System.out.println("thread 2 update from " + value + " to 2"); // do sth value = atomicStampedReference.get(stampHolder); stamp = stampHolder[0]; System.out.println("thread 2 read value: " + value + ", stamp: " + stamp); if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) { System.out.println("thread 2 update from " + value + " to 1"); } } }).start(); } }
- 循环可能导致 CPU 开销过大
在 unsafe 源码中可以看出,若未能按照期望值更新,该方法会一直循环下去,这对 CPU 的开销是极大的。
可以通过增加次数限制解决该问题。 - 只能保证一个共享变量的原子操作
如果要保证一个代码块的原子性,CAS 则不支持。此时,可以通过加锁,或者将多个共享变量转化为一个执行 CAS。
参考文章:死磕 java 并发包之 AtomicStampedReference 源码分析(ABA 问题详解) - 掘金 (juejin.cn)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于