CAS&Atomic原子操作详解
CAS&Atomic原子操作详解
penjc1. 什么是原子操作?如何实现原子操作?
事务的一大特性就是原子性(事务具有 ACID 四大特性)。一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。
并发中的原子性和原子操作具有相同的内涵和概念。假定有两个操作 A 和 B 都包含多个步骤,如果从执行 A 的线程来看,当另一个线程执行 B 时,要么将 B 全部执行完,要么完全不执行 B。执行 B 的线程看 A 的操作也是一样的,那么 A 和 B 对彼此来说是原子的。
如何实现原子操作?
1. 使用锁机制
锁机制可以满足基本的需求,但是存在一些问题:
- 阻塞问题:当一个线程拥有锁时,访问同一资源的其它线程需要等待,直到该线程释放锁。
- 优先级反转:如果被阻塞的线程优先级很高会影响系统的整体性能。
- 死锁风险:如果获得锁的线程一直不释放锁,可能会造成死锁。
- 粒度较大:对于某些精细的需求,如计数器操作,锁机制可能显得笨重。
详细解释
实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized 关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,
这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。
为了解决这个问题,Java 提供了 Atomic 系列的原子操作类。
2. CAS(Compare And Swap)
Java 提供了 Atomic 系列的原子操作类,它们利用了 CAS 指令进行原子操作。
CAS 操作过程包含三个元素:
- V:内存地址
- A:期望值
- B:新值
如果内存地址上的值等于期望值 A,则将其更新为 B,否则不做任何操作。
CAS 操作存在 循环 CAS,即如果 CAS 失败,则进行重试,直到成功。
2. CAS 实现原子操作的三大问题
1. ABA 问题
CAS 依赖于检查值是否发生变化,如果一个值从 A 变成 B 又变回 A,则 CAS 可能检测不到变化。
解决方案:使用版本号,每次变量更新时增加版本号。例如:1A → 2B → 3A
。
如JUC中atomic中AtomicStampedReference
和AtomicMarkableReference
类,通过引入版本号或标记解决了 ABA 问题。
2. 自旋开销大
自旋 CAS 在高并发情况下如果长时间不成功,会给 CPU 带来较大的执行开销。
3. 只能保证一个共享变量的原子操作
如果多个共享变量需要保证原子操作,则需要使用 锁机制,或者使用 AtomicReference
将多个变量封装到一个对象中。
引用类型的原子操作类
1 | /** |
带版本戳的原子操作类
1 | /** |
3. JDK 提供的原子操作类API使用
1. AtomicInteger
int addAndGet(int delta)
:以原子方式增加值,并返回结果。boolean compareAndSet(int expect, int update)
:如果当前值等于预期值,则更新为新值。int getAndIncrement()
:以原子方式将当前值加 1,返回原值。int getAndSet(int newValue)
:以原子方式设置新值,并返回旧值。
2. AtomicIntegerArray
用于以原子方式更新数组里的整数。
int addAndGet(int i, int delta)
:对数组索引i
处的值增加delta
。boolean compareAndSet(int i, int expect, int update)
:如果索引i
处的值等于expect
,则更新。
3. 原子更新引用类型
AtomicReference
:原子更新引用类型。AtomicStampedReference
:使用版本戳防止 ABA 问题。AtomicMarkableReference
:原子更新带有标记位的引用类型。
4. 原子更新字段类
AtomicIntegerFieldUpdater
:原子更新整型字段。AtomicLongFieldUpdater
:原子更新长整型字段。AtomicReferenceFieldUpdater
:原子更新引用类型字段。
LongAdder
LongAdder
是 JDK 1.8 引入的 高并发优化 版本的 AtomicLong
,它通过 热点分散 方式优化性能。
AtomicLong
的问题
AtomicLong
采用 CAS 实现,当多个线程竞争时,会出现 自旋失败并不断重试 的情况,导致性能下降。
LongAdder
的优化
LongAdder
通过 分散热点,将 value
分散到一个 Cell[]
数组中,每个线程只修改自己的槽位,降低了竞争概率。
base
变量:无竞争时直接累加。Cell[]
数组:高并发时,线程更新各自的槽位。
在实际运用的时候,只有从未出现过并发冲突的时候,base基数才会使用到,一旦出现了并发冲突,之后所有的操作都只针对Cell[]数组中的单元Cell。
通过cell的计算结果为
LongAdder最终结果的求和,并没有使用全局锁,返回值不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,所以只能得到某个时刻的近似值,这也就是LongAdder并不能完全替代LongAtomic的原因之一。
适用场景:
AtomicLong
适用于低并发、需要精确控制计数。LongAdder
适用于高并发 写多读少 的场景。
详细解释
LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
这种做法和ConcurrentHashMap中的“分段锁”其实就是类似的思路。LongAdder提供的API和AtomicLong比较接近,两者都能以原子的方式对long型变量进行增减。
但是AtomicLong提供的功能其实更丰富,尤其是addAndGet、decrementAndGet、compareAndSet这些方法。
addAndGet、decrementAndGet除了单纯的做自增自减外,还可以立即获取增减后的值,而LongAdder则需要做同步控制才能精确获取增减后的值。如果业务需求需要精确的控制计数,做计数比较,AtomicLong也更合适。