前言
学习情况记录
- 时间:week 1
- SMART 子目标:Java 多线程
记录在学习线程安全知识点中,关于 CAS
的有关知识点。
线程安全是指:多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
常见的线程安全实现方法分为不可变对象、线程互斥同步、非阻塞同步、线程本地存储等方案,本文要讲的就是非阻塞同步中的核心CAS
.
非阻塞同步
从处理问题的方式上说,互斥同步属于一种悲观的并发策略。
随着 硬件指令集 的发展,我们可以采用 基于冲突检查的乐观并发策略,通俗地说,就是先行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现偶读不需要把线程挂起,因此这种同步操作称为非阻塞同步。
CAS
乐观锁需要 操作和冲突检测 这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成 。 硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
各种 Atomic 开头的原子类,内部都应用到了 CAS
。就拿AtomicInteger
为例。
J.U.C 包里面的原子类 AtomicInteger
的方法调用了 Unsafe
类的 CAS
操作。
看看 AtomicInteger
对象一次自增,CAS
起了什么作用,以下代码是 incrementAndGet()
的源码,可以看到内部调用了 Unsafe
对象的 getAndAddInt()
。
以下代码是 getAndAddInt()
源码,var1
指示对象内存地址,var2
指示该字段相对对象内存地址的偏移,var4
指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2)
得到旧的预期值,通过调用 compareAndSwapInt()
来进行 CAS
比较 , 如果该字段内存地址中的值等于 var5
,那么就更新内存地址为 var1+var2
的变量为 var5+var4
。
compareAndSwapInt(var1, var2, var5, var5 + var4
其实换成 compareAndSwapInt(obj, offset, expect, update)
比较清楚,意思就是如果 obj
内的 value
和expect
相等,就证明没有其他线程改变过这个变量,那么就更新它为 update
,如果这一步的CAS
没有成功,那就采用自旋的方式继续进行 CAS 操作,取出乍一看这也是两个步骤了啊,其实在 JNI 里是借助于一个 CPU 指令完成的。所以还是原子操作。
CAS 的问题
-
ABA 问题
- 描述:如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
- 解决方案:J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果真的需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
-
循环时间长开销大
- 自旋
CAS
(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来比较大的执行开销。
- 自旋
-
只能保证一个共享变量的原子操作
-
CAS
只对单个共享变量有效,当操作涉及跨多个共享变量时CAS
无效。但是从 JDK 1.5 开始,提供了AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS
操作. 所以我们可以使用锁或者利用AtomicReference
类把多个共享变量合并成一个共享变量来操作。
-
CAS 与 synchronized 的使用情景
- 简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少)
- synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)
- 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
CAS 的应用
使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。关于非阻塞算法是属于 J.U.C 中并发容器部分的知识,属于比较难的内容。目前先引用几篇文章。作为记录。
- 非阻塞算法在并发容器中的实现
- 非阻塞同步算法实战(一)
- 非阻塞同步算法实战(二)-BoundlessCyclicBarrier
- 非阻塞同步算法实战(三)-LatestResultsProvider
参考
- 《深入理解 Java 虚拟机》
- https://www.ibm.com/developer…