Java 并发:原子类的实现(CAS 算法)
什么是 CAS?
CAS:Compare and Swap,即比较再交换。
JDK5 增加了并发包java.util.concurrent.*
,其下面的类使用 CAS 算法实现了区别于 synchronized 同步锁的一种乐观锁。JDK 5 之前 Java 语言是靠 synchronized 关键字保证同步的,这是一种独占锁,也是悲观锁。
CAS 算法理解
CAS 是一种无锁算法,CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。当然如果需要的话,可以设计成自旋锁的模式,循环判断 V 与 A 是否相等。
CAS 比较与交换的伪代码可以表示为:
do { 备份旧数据; 基于旧数据构造新数据; } while (!CAS(内存地址, 备份的旧数据, 新数据))
比如:t1,t2 线程都尝试修改某变量的值(变量初始值为 56)。
t1 和 t2 线程都读取到变量初始值 56,它们会把主内存的值拷贝一份到自己的工作内存空间,所以 t1 和 t2 线程的预期值都为 56。
假设 t1 在与 t2 线程竞争中线程 t1 先去更新变量的值(尝试更新为 57),发现预期值和内存值都为 56,于是更新变量值为 57,然后写到内存中。此时对于 t2 来说,内存值变为了 57,与预期值 56 不一致,就操作失败了(想改的值不再是原来的值),失败后 t2 并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试。
就是指当预期值和内存值进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明数据已经被修改,放弃已经完成的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
原子类
某个操作是不可分割的,要么成功,要么失败,不会存在中间状态,那么我们说这个操作是原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(synchronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
i++ 不是原子操作,除了在 i++ 操作时使用 synchronized 关键字实现同步外,还可以使用 AtomicInteger 原子类实现累加。
AtomicInteger 在 java.util.concurrent.atomic 包中,该包提供了以下原子类,它们是线程安全的类。
- AtomicBoolean -- 原子布尔
- AtomicInteger -- 原子整型
- AtomicIntegerArray -- 原子整型数组
- AtomicLong -- 原子长整型
- AtomicLongArray -- 原子长整型数组
- AtomicReference -- 原子引用
- AtomicReferenceArray -- 原子引用数组
- AtomicMarkableReference -- 原子标记引用
- AtomicStampedReference -- 原子戳记引用
- AtomicIntegerFieldUpdater -- 用来包裹对整形 volatile 域的原子操作
- AtomicLongFieldUpdater -- 用来包裹对长整型 volatile 域的原子操作
- AtomicReferenceFieldUpdater -- 用来包裹对对象 volatile 域的原子操作
Java 提供的原子类是基于 CAS 实现的,CAS 是一种乐观锁。
AtomicInteger 表示一个 int 类型的值,并提供了 get 和 set 方法,它还提供了一个原子的 compareAndSet 方法,以及原子的添加、递增和递减等方法。当多线程竞争时,AtomicInteger 的原子性操作保证值能够被正确更新。
AtomicInteger 的实现
AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger,要实现一个递增 ID 生成器,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。
接下来通过源代码来看 AtomicInteger 具体是如何实现的原子操作。
首先看 value 的声明:
private volatile int value;
volatile 修饰的 value 变量,保证了变量的可见性。
incrementAndGet() 方法,下面是具体的代码:
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
通过源码,可以知道,这个方法的做法为先获取到当前的 value 属性值,然后将 value 加 1,赋值给一个局部的 next 变量,然而,这两步都是非线程安全的,但是内部有一个死循环,不断去做 compareAndSet 操作,直到成功为止,也就是修改的根本在 compareAndSet 方法里面,compareAndSet() 方法的代码如下:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
compareAndSet() 方法调用的 compareAndSwapInt() 方法的声明如下,是一个 native 方法。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
compareAndSet 传入的为执行方法时获取到的 value 属性值,next 为加 1 后的值,compareAndSet 中调用 unsafe 的 compareAndSwapInt 方法来完成操作,此方法为 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的、compareAndSwapInt 方法保证原子性。基于 CAS 指令的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好。
类似地,还有 decrementAndGet() 方法。它和 incrementAndGet() 的区别是将 value 减 1,赋值给 next 变量。
AtomicInteger 中还有 getAndIncrement() 和 getAndDecrement() 方法,他们的实现原理和上面的两个方法完全相同。还有很多的其他方法,就不列举了。
考虑如下问题:
情况 1
两个线程 A 和 B 同时对 AtomicInteger(10) 进行 incrementAndGet() 方法,都获取到 current = 10,compareAndSet 比较时,均发现内存值未被修改,那两个线程都将执行了 +1,那返回的结果应该都为 11 吧?
情况 2
两个线程 A 和 B 都对 AtomicInteger(10) 进行 incrementAndGet() 方法,都获取到 current = 10,线程 A 线程先进行了 compareAndSwapInt 导致内存中的值变为 11,那线程 B 的在和内存中的值比较一直不相等,那线程 B 不是死循环了吗?
解决问题:
情况 1:不会存在返回结果都是 11 的情况。原子类提供的就是原子操作,多线程情况下不会存在数据不一致的情况。具体原因就是 CAS 操作,它会读取内存和预期值(11)作比较,如果相同才会进行赋值。
情况 2:同理。不会死循环,B 比较发现不相等时,会重新获取内存值。
CAS 的三大问题
尽管 CAS 提供了一种有效的同步手段,但也存在一些问题,主要有以下三个:ABA 问题、长时间自旋、多个共享变量的原子操作。
ABA 问题
所谓的 ABA 问题,就是一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个 AtomicStampedReference 类来解决 ABA 问题。
长时间自旋
CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。
解决思路是让 JVM 支持处理器提供的 pause 指令。
pause 指令能让自旋失败时 CPU 睡眠一小段时间再继续自旋,从而使得读操作的频率降低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。
多个共享变量的原子操作
当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:
- 使用
AtomicReference
类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作; - 使用锁,锁内的临界区代码可以保证只有当前线程能操作。
扩展:深入浅出 CAS
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix