CAS&Atomic原子操作详解

1. 什么是原子操作?如何实现原子操作?

事务的一大特性就是原子性(事务具有 ACID 四大特性)。一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。

并发中的原子性和原子操作具有相同的内涵和概念。假定有两个操作 A 和 B 都包含多个步骤,如果从执行 A 的线程来看,当另一个线程执行 B 时,要么将 B 全部执行完,要么完全不执行 B。执行 B 的线程看 A 的操作也是一样的,那么 A 和 B 对彼此来说是原子的。

如何实现原子操作?

1. 使用锁机制

锁机制可以满足基本的需求,但是存在一些问题:

  • 阻塞问题:当一个线程拥有锁时,访问同一资源的其它线程需要等待,直到该线程释放锁。
  • 优先级反转:如果被阻塞的线程优先级很高会影响系统的整体性能。
  • 死锁风险:如果获得锁的线程一直不释放锁,可能会造成死锁。
  • 粒度较大:对于某些精细的需求,如计数器操作,锁机制可能显得笨重。
详细解释

实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized 关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,
这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。
为了解决这个问题,Java 提供了 Atomic 系列的原子操作类。

2. CAS(Compare And Swap)

Java 提供了 Atomic 系列的原子操作类,它们利用了 CAS 指令进行原子操作。

CAS

CAS 操作过程包含三个元素:

  • V:内存地址
  • A:期望值
  • B:新值

如果内存地址上的值等于期望值 A,则将其更新为 B,否则不做任何操作。

CAS 操作存在 循环 CAS,即如果 CAS 失败,则进行重试,直到成功。

2. CAS 实现原子操作的三大问题

1. ABA 问题

CAS 依赖于检查值是否发生变化,如果一个值从 A 变成 B 又变回 A,则 CAS 可能检测不到变化。

解决方案:使用版本号,每次变量更新时增加版本号。例如:1A → 2B → 3A

如JUC中atomic中AtomicStampedReferenceAtomicMarkableReference类,通过引入版本号或标记解决了 ABA 问题。

2. 自旋开销大

自旋 CAS 在高并发情况下如果长时间不成功,会给 CPU 带来较大的执行开销。

3. 只能保证一个共享变量的原子操作

如果多个共享变量需要保证原子操作,则需要使用 锁机制,或者使用 AtomicReference 将多个变量封装到一个对象中。

引用类型的原子操作类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
*类说明:演示引用类型的原子操作类
*/
public class UseAtomicReference {
static AtomicReference<UserInfo> atomicUserRef;
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
atomicUserRef = new AtomicReference(user);
UserInfo updateUser = new UserInfo("Bill",17);
atomicUserRef.compareAndSet(user,updateUser);

System.out.println(atomicUserRef.get());
System.out.println(user);
}

//定义一个实体类
static class UserInfo {
private volatile String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}

@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
带版本戳的原子操作类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
*类说明:演示带版本戳的原子操作类
*/
public class UseAtomicStampedReference {
static AtomicStampedReference<String> asr
= new AtomicStampedReference("mark",0);

public static void main(String[] args) throws InterruptedException {
//拿到当前的版本号(旧)
final int oldStamp = asr.getStamp();
final String oldReference = asr.getReference();
System.out.println(oldReference+"============"+oldStamp);

Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":当前变量值:"
+oldReference + "-当前版本戳:" + oldStamp + "-"
+ asr.compareAndSet(oldReference,
oldReference + "+Java", oldStamp,
oldStamp + 1));
}
});

Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName()
+":当前变量值:"
+reference + "-当前版本戳:" + asr.getStamp() + "-"
+ asr.compareAndSet(reference,
reference + "+C", oldStamp,
oldStamp + 1));
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();

System.out.println(asr.getReference()+"============"+asr.getStamp());
}
}

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最终结果的求和,并没有使用全局锁,返回值不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,所以只能得到某个时刻的近似值,这也就是LongAdder并不能完全替代LongAtomic的原因之一。

适用场景

  • AtomicLong 适用于低并发、需要精确控制计数。
  • LongAdder 适用于高并发 写多读少 的场景。
详细解释

LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
这种做法和ConcurrentHashMap中的“分段锁”其实就是类似的思路。LongAdder提供的API和AtomicLong比较接近,两者都能以原子的方式对long型变量进行增减。
但是AtomicLong提供的功能其实更丰富,尤其是addAndGet、decrementAndGet、compareAndSet这些方法。
addAndGet、decrementAndGet除了单纯的做自增自减外,还可以立即获取增减后的值,而LongAdder则需要做同步控制才能精确获取增减后的值。如果业务需求需要精确的控制计数,做计数比较,AtomicLong也更合适。

其他新增原子类

其他新增原子类

LongAccumulator

LongAccumulatorLongAdder 的增强版,可以使用 自定义函数 进行计算,而不仅仅是加减。

1
2
LongAccumulator accumulator = new LongAccumulator(Long::sum, 0);
accumulator.accumulate(10);

DoubleAdderDoubleAccumulator

用于 double 类型的原子计算。