JohnShen's Blog.

[回顾并发基础] 原子操作类

字数统计: 2.4k阅读时长: 9 min
2019/10/08 Share

介绍了原子操作类,以AtomicLong介绍原子类的实现,并对原子类的分类和使用(基本数据类型、引用类型、对象属性更新器、数组、累加器)进行说明。

简介

JUC中对于简单的原子性问题提炼封装成一系列的原子类,这是一种无锁方案。相对于互斥锁而言,其最大的好处早会性能,互斥锁为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性。

原子类使用到了硬件的支持,CPU为了解决并发问题,提供了CAS (Compare And Swap) 指令,CAS 指令包含三个参数:共享变量的内存地址 A、用于比较的值 B 和 共享变量的新值 C只有内存地址中 A 处的值等于 B 时,才能将内存地址 A 处的值更新为新值 C且作为一条 CPU 指令,CAS 本身能够保证原子性

当然,CAS 不是完美无缺的。CAS 方案会有 ABA 问题,即值被更新后,后又被更新为原值。是否需要关系 ABA 问题也需要依照特定情况而言,可以使用版本号标识解决 ABA 问题,后续会进行介绍。

实现

Atomic包里的类基本都是使用Unsafe实现的包装类。接下来以AtomicLong的方式先介绍典型实现。

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
45
46
47
48
49
50
51
52
53
54
55
public class AtomicLong extends Number implements java.io.Serializable {
private static final long serialVersionUID = 1927816293512124184L;

// 获取Unsafe示例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 存放变量value的偏移量
private static final long valueOffset;

// 判断JVM是否支持Long类型无锁CAS
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
private static native boolean VMSupportsCS8();

static {
try {
// 获取value在AtomicLong中的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 实际变量值value
private volatile long value;

public AtomicLong(long initialValue) {
value = initialValue;
}

public AtomicLong() {}

// 调用unsafe方法, 原子性设置value值为原始值+1, 返回值为原始值。这里 this 和 valueOffset 两个参数可以唯一确定共享变量的内存地址。
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}

// 调用unsafe方法, 原子性设置value值为原始值+1, 返回值为递增后的值
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

// 原子类的重要方法,更新成功返回true, 失败则返回false
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

...
}

// Unsafe的getAndAddLong方法
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
// Unsafe的compareAndSwapLong方法
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上述getAndAddLong方法就是 CAS 经典使用案例。可以看出,使用 CAS 来解决并发问题一般都会伴随着自旋。一般的无锁代码经常会采用如下的方式,使用的是compareAndSet方法。

1
2
3
4
5
6
do {
// 获取当前值
oldV = xxxx;
// 根据当前值计算新值
newV = ...oldV...
} while (!compareAndSet(oldV, newV);

分类

原子类可以按照以下的方式进行划分:

类型 具体类
基本数据类型 AtomicBoolean、AtomicInteger、AtomicLong
引用类型 AtomicReference、AtomicStampedReference、AtomicMarkableReference
对象属性更新器 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
数组 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
累加器 DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder

AtomicIntegerArray将数组value传递给构造方法,然后会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

1
2
3
4
5
6
7
public static void main(String[] args) {
int[] value = new int[]{1, 2};
AtomicIntegerArray ai = new AtomicIntegerArray(value);
System.out.println(ai.getAndSet(0, 3)); // 1
System.out.println(ai.get(0)); // 3
System.out.println(value[0]); // 1
}

原子更新基本类型只能更新一个变量,如果要原子更新多个变量,就需要使用原子更新引用类型提供的类,以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
public static void main(String[] args) {
AtomicReference<User> atomicUserRef = new AtomicReference<>();
User user = new User("user1", 25);
atomicUserRef.set(user);
User updateUser = new User("user2", 26);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getAge());
}

static class User {
private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

上文提到解决ABA问题可以使用版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来。AtomicStampedReference 实现的 CAS 方法就增加了版本号参数,方法签名如下:

1
2
3
4
5
boolean compareAndSet(
V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// initialStamp 为 0
AtomicStampedReference<String> reference = new AtomicStampedReference<>("Atomic", 0);
int stamp = reference.getStamp();
String obj = reference.getReference();
System.out.println(reference.compareAndSet(obj, "AtomicNew", stamp, stamp + 1)); // true
System.out.println(reference.getReference()); // AtomicNew
System.out.println(reference.getStamp()); // 1
}

AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值,方法签名如下:

1
2
3
4
5
boolean compareAndSet(
V expectedReference,
V newReference,
boolean expectedMark,
boolean newMark)

如果需要原子地更新某个类里的某个字段时,可以使用原子更新字段类,以AtomicIntegerFieldUpdater举例:

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
public static void main(String[] args) {
// 创建原子更新器, 并设置需要更新的对象类和对象的属性
AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater
.newUpdater(User.class, "age");
User conan = new User("user1", 25);
System.out.println(updater.getAndIncrement(conan));
System.out.println(updater.get(conan));
}

static class User {
private String name;
/**
* 更新类的属性必须使用 public volatile 修饰符
**/
public volatile int age;

public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

DoubleAccumulatorDoubleAdderLongAccumulatorLongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。JDK8 之所以会新增这些类,是因为:原子类在高并发下大量线程会同时取竞争更新同一个原子变量,但由于只有一个线程 CAS 成功,就会造成大量线程竞争失败,会通过无限循环不断进行自旋尝试 CAS 操作,而这会浪费CPU资源。

以 LongAdder 为例,既然 AtomicLong 性能瓶颈在于多线程竞争一个变量的更新而产生的,那就把一个变量分解为多个变量,让多线程竞争多个资源。LongAdder 就是以这种方式进行设计的,其内部维护着一个延迟初始化的原子性更新数组(默认情况下Cell 数组是null)和一个基值变量base:

  • 其内部维护多个 Cell 变量,每个 Cell 有一个初始值为 0 的 long 类型变量,同等并发量的情况下争夺单个变量的线程量会减少。
  • 此外,多个线程争夺同一个 Cell 失败后并不会在当前在当前 Cell 变量上一直自旋 CAS 重试,而是尝试在其他 Cell 的变量上进行 CAS 尝试,以增加当前线程重试 CAS 成功的概率。
  • 当最后获取累加值时就是将所有 Cell 变量的 value 值累加后加上 base 后返回的。
  • 原子性数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,因此使用@sun.misc.Contended 注解对 Cell 类进行字节填充,以避免数组中多个元素共享缓存行(避免伪共享)。

关于伪共享

为了解决CPU和主内存间的速度差异,之间会添加一级或多级告诉缓冲存储器(Cache),而 Cache 内部是按行存储的,每行称为一个 Cache 行,Cache 行的大小一般为 2 的幂次数字节,这是 Cache 与主内存进行数据交换的单位。

当CPU访问变量不存在时,先去 Cache 中看存不存在,没有的话就去内存取,然后将该变量所在内存区域(大小相当于 Cache 行大小)复制到 Cache 中,注意不是复制单个变量而是复制内存块。当多个线程同时修改一个缓存行里面的多个变量时,由于每时刻只能有一个线程操作缓存行,所以相对于将每个变量放到单独缓存行中,性能会有所下降,这就是伪共享。

当两个不同线程同时进行操作时,线程1使用CPU1更新变量x,那么在缓存一致性协议下,CPU2中变量x对应的缓存行会失效,线程2想要对与x在同一缓存行中的y进行操作时,就只能去下一级缓存中寻找。

一般通过字节填充的方式解决伪共享,JDK8 中提供 @sun.misc.Contended注解就可解决伪共享,可用于修饰类,也可以修饰变量。此注解只能用于 rt 包下的核心类,用户类路径下想使用,需要添加 JVM 参数:-XX:-RestrictContended。填充的宽度默认为128 ,要自定义宽度则可以设置-XX:ContendedPaddingWidth 参数。 (from 《Java并发编程之美》)


Reference

https://time.geekbang.org/column/article/90515

《Java并发编程的艺术》

《Java并发编程之美》

CATALOG
  1. 1. 简介
  2. 2. 实现
    1. 2.1. 分类
  3. 3. Reference