JVM 的Synchrod 轻量级锁使用 CAS 进行自旋抢锁,并且处于用户态下,所以轻量级锁开销较小。
3.1 什么是 CAS
JDK 5 增加的 JUC (java.util.concurrent) 并发包对操作系统的底层 CAS 原子操作进行了封装,为上层提供了 CAS 操作的 API 。
3.1.1 Unsafe 类中的 CAS 方法
Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全的底层操作,如直接访问系统内存资源、自主管理内存资源等。从名字都可以看出这个类对普通程序员来说是“危险”的,官方也不建议直接在程序中使用这些类。
获取 Unsafe 实例
Unsafe 类时一个final 修饰的不允许继承的类,并且构造函数是 private 类型,源码如下:
1 | public final class Unsafe { |
所以我们无法在外部对 Unsafe 实例化,那么应该怎么获取呢?可以通过反射方式获取 theUnsafe 实例:
1 | public class JvmUtil { |
Unsafe提供的 CAS 方法
总共提供了如下3种方法:
1 | public final boolean compareAndSwapObject(Object o, long offset, Object expected,Object x) |
这些方法首先将内存位置的值与预期值比较,如果相匹配,那么CPU 会自动将该内存位置的值更新为新值,并返回true;否则,CPU不做任何操作,并返回false。
3.1.2 使用 CAS 进行无锁编程
CAS 是一种无锁算法,底层CPU 利用原子操作判断 内存值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作。使用 CAS 进行无锁编程的步骤大概如下:
获得字段的期望值(oldValue)
计算出需要替换的新值(newValue)
通过 CAS 将 newValue 放在字段的内存地址上,如果 CAS 失败就重复从第1步开始,直到 CAS 成功。这种重复俗称“自旋”
举例: 假如 2 个线程 A 和B 对一个共享变量做 +1 操作,用 CAS 去做这个操作。但是线程是并发进行的,假如 A 和 B 都读到旧值是 1 ,然后并发通过 CAS 操作,都是 CAS(1, 2) ,但是CAS 是原子操作,同一个内存地址的 CAS 在同一个时刻只能执行一个,因此,假设 A 先执行,A 的 CAS(1, 2) 因为期望值是1,内存值也是1,操作成功,返回true;接下来 B 执行 CAS(1, 2) 肯定会失败了,因为内存值目前已经是 2 了,而期望值是 1 ,所以只得重新获取得到期望值 2,计算出新的值 3, 最后通过 CAS(2, 3) 才能成功。
3.2 JUC原子类
并发执行时,诸如 ++ 或者 – 类的运算不具备原子性,大家可能会用 synchronized 方法做同步,但效率肯定会影响的。JDK 为这些类型不安全的操作提供了一些原子类,与 synchronized 相比效率会更高。
3.2.1 JUC中的Atomic 原子操作包
只需要知道有:
基本原子类:AtomicInteger、AtomicLong、AtomicBoolean
数组原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
引用原子类:AtomicReference、AtomicMarkableReference、AtomicStampedReference
等等一些常见的即可。
3.4 ABA问题
什么是 ABA 问题,举个例子:比如线程 A 从内存位置 M 取出 V1,另一个线程 B 也取出 V1 ,假设 B 进行了一些操作后将 M 位置的数据 V1 变成了 V2,然后又在一些操作之后将 V2 变成了 V1,然后线程 A 通过 CAS 操作时发现条件满足,CAS 操作成功。
但是这个过程是有问题的,A 操作的时候 V1 已经不是以前的 V1 了,这就是 ABA 问题。
3.4.2 ABA问题的解决方案
很多乐观锁的实现版本是: 使用版本号(Version)方式来解决ABA问题。 每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作,否则执行失败。因为操作的版本号只会增加,不会减少。
当然,参考乐观锁的版本号实现, JDK 提供了一个 AtomicStampedReference 类来解决 ABA 问题,AtomicStampedReference 在 CAS 的基础上增加了一个 Stamp(印戳或标记)来察觉数据是否发生了变化。
当然,还可以使用 AtomicMarkableReference 解决。它是 AtomicStampedReference 的简化版,不关心修改过几次,只关心是否修改过。
3.5 提升高并发场景下 CAS 操作的性能
在竞争激烈的场景下,会导致大量的 CAS 自旋,比如大量线程同时并发修改一个 AtomicInteger 是,很多线程可能不停地自旋, 这浪费了大量的 CPU。
3.5.1 以空间换时间:LongAdder
AtomicLong 使用内部变量 value 保存着实际的 long 值,所有操作都是针对该 value 的,也就是说,当高并发的情况下,value 变量其实是一个热点,N 个线程竞争这一个热点,重试的线程越多,意味着 CAS 失败的概率越高。
LongAdder 的核心思想是热点分离,与 ConcurrentHashMap 的设计思想类似:将 value 值分离成一个数组,当多线程访问时,通过 Hash 算法将线程映射到数组的一个元素进行操作;而获取最终value 结果时,则将数组的元素求和。
具体一点:LongAdder 将 value 值分散到一个数组中,不同的线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行 CAS 操作,这样热点就被分散了,这样,即使线程数再多也不担心,各个线程分配到多个元素更新。如果要获取完整的 LongAdder 存储的值,只要将各个槽中的变量值累加即可
在 CAS 竞争非常激烈的场景, LongAdder 的性能可达到 AtomicLong 的 8 倍。
3.6 CAS在JDK中的广泛应用
3.6.1 CAS操作的弊端和规避措施
CAS 操作弊端主要有下面 3 点:
ABA问题,解决思路: 添加版本号、使用 AtomicStampedReference、AtomicMarkableReference,其中前者比较常用
只能保证一个共享变量的原子操作。解决思路:将多个共享变量合并成一个共享变量来操作
开销问题。解决思路:分散操作热点(如 LongAdder)、使用队列削峰(将 发生 CAS 争用的线程加入一个队列中排队,降低 CAS 争用的激烈程度,JUC 中非常重要的基础类 AQS 就是这么做的!)
3.6.2 CAS 在JDK中的应用
CAS 在 在java.util.concurrent.atomic包中的原子类、Java AQS 以及 显式锁、ConcurrentHashMap 等重要并发容器中都有非常广泛的应用