0%

第3章:CAS原理与JUC原子类

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
2
3
4
5
6
7
8
9
10
11
public final class Unsafe {

static {
Reflection.registerMethodsToFilter(Unsafe.class, Set.of("getUnsafe"));
}

private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();
...
}

所以我们无法在外部对 Unsafe 实例化,那么应该怎么获取呢?可以通过反射方式获取 theUnsafe 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JvmUtil {
//自定义地获取Unsafe实例的辅助方法
public static Unsafe getUnsafe() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
}
// 省略不相干代码
}
Unsafe提供的 CAS 方法

总共提供了如下3种方法:

1
2
3
4
5
public final boolean compareAndSwapObject(Object o, long offset, Object expected,Object x) 

public final boolean compareAndSwapInt(Object o, long offset, int expected, int x)

public final boolean compareAndSwapLong(Object o, long offset,long expected, long x)

这些方法首先将内存位置的值与预期值比较,如果相匹配,那么CPU 会自动将该内存位置的值更新为新值,并返回true;否则,CPU不做任何操作,并返回false。

3.1.2 使用 CAS 进行无锁编程

CAS 是一种无锁算法,底层CPU 利用原子操作判断 内存值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作。使用 CAS 进行无锁编程的步骤大概如下:

  1. 获得字段的期望值(oldValue)

  2. 计算出需要替换的新值(newValue)

  3. 通过 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 等重要并发容器中都有非常广泛的应用

谢谢你的鼓励