Java多线程笔记(4)-CAS与Atomic原子类

CAS

CAS,全称为Compare-And-Set / Compare-And-Swap,它是一个CPU并发原语,保证其是一个原子操作

CAS完成一个变量的交换操作,需要提供变量的预期值A以及设置值B,然后进行CAS原子操作。CAS会先检测当前变量的值是否是预期值A,如果是就将其赋值为设置值B,如果不是则交换失败,对变量不做其他操作

CAS的操作必须需要借助volatile,读取到共享变量的最新值,来实现比较并交换的效果。并且在使用的时候,通常会配合while使用,通过自旋+CAS来实现无锁并发的效果

1
2
3
4
5
6
// 自旋+CAS实现无锁并发的伪代码
while(true){
int prev = xxx;
int next = yyy;
CAS(target, prev, next);
}

结合CAS和volatile可以实现无锁并发,基于乐观锁的思想,适用于线程数少,多核CPU的场景下,最好不要超过CPU的核心数。因为没有使用synchronzied,所以线程不会陷入阻塞,这也是效率提升的因素之一。但是如果竞争激烈,可以想到的是重试必然频繁发生,效率反而受到影响

CAS本身也有一些缺点:

  • 循环时间长,开销大。如果CAS比较不成功会一直空转,在竞争激烈的情况下会导致效率很低。因此在使用CAS的时候,最好线程数不要超过CPU的核心数
  • CAS只能保证一个共享变量的原子操作,如果对于多个共享变量,循环+CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • ABA问题。考虑如下场景,线程1希望将共享变量从A修改成C,但是过程中有其他线程先将这个共享变量从A修改成了B,又将其从B修改成了A,线程1仍然能够达到修改效果,但是期间有其他线程修改了共享变量,和我们的预期效果有所不符。这个问题可以通过增加版本来解决,在后续的原子引用中会继续提到

Java中的CAS,底层都是通过sun.misc.Unsafe类来完成的。在Unsafe类中提供了一个单例的Unsafe对象,其中的所有方法都是native方法,直接调用操作系统底层资源执行相应的任务

这里,Unsafe指的并不是线程安全,而是说这个类是直接调用操作系统底层的资源,因此不建议程序员直接使用。在Unsafe类中的单例对象也是使用了private进行修饰,无法直接获得,但是可以通过反射进行获取

Unsafe类中,提供的与CAS相关的方法有getAndSetIntgetAndSetLonggetAndSetObject

原子类

利用循环+CAS机制可以实现无锁并发,但是如果由程序员直接使用的话会较为复杂,因此Java中也封装了一些比较方便的方法。java.util.concurrent.atomic包提供了许多利用CAS机制实现的原子类和工具类,这些类可以保证不同共享变量的原子性

原子基本类型

原子基本类型可以保证基本类型的原子性,常用的类有AtomicIntegerAtomicBooleanAtomicLong,下面简单介绍其中原子整数的使用

构造方法:

  • public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
  • public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer

常用API:

方法 作用
public final int get() 获取 AtomicInteger 的值
public final boolean compareAndSet(int expect, int update) 原子方式CAS,判断当前元素值是否是expect
如果是则将其设置为update并返回true,否则返回false
public final int getAndIncrement() 以原子方式将当前值加 1,返回的是自增前的值
public final int incrementAndGet() 以原子方式将当前值加 1,返回的是自增后的值
pubilc final int getAndDecrement() 以原子方式将当前值减1,返回的是自减前的值
public final int decremantAndGet() 以原子方式将当前值减1,返回的是自减后的值
public final int getAndSet(int value) 以原子方式设置为 newValue 的值,返回旧值
public final int addAndGet(int data) 以原子方式将输入的数值与实例中的值相加并返回
public final int getAndUpdate(IntUnaryOperator updateFunction) 提供一个函数式接口,描述对元素值的操作,以原子方式进行操作
返回旧值
public final int updateAndGet(IntUnaryOperator updateFunction) 提供一个函数式接口,描述对元素值的操作,以原子方式进行操作
返回进行操作后的值

原子引用

如果需要保证一个对象的原子性,则需要使用原子引用类。原子引用类可以对某个对象进行原子操作。常用的原子引用类有AtomicReferenceAtomicMarkableReferenceAtomicStampedReference

AtomicReference类的使用:

  • 构造方法:AtomicReference<T> atomicReference = new AtomicReference<T>()

  • 常用 API:

    • public final boolean compareAndSet(V expectedValue, V newValue):CAS 操作
    • public final void set(V newValue):将值设置为 newValue
    • public final V get():返回当前值

前面我们提到了CAS中会存在的ABA问题,我们可以使用版本号来解决这种问题。AtomicStampedReference类就是带版本的原子类。考虑下面的代码,模拟ABA问题的流程:

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
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

public static void main(String[] args) throws InterruptedException {
// 打印初始状态
log.debug("main start...");
String prev = ref.getReference();
int startStamp = ref.getStamp();
log.debug("当前状态: ref:{}, stamp:{}", prev, startStamp);

// thread1先将A修改成B
new Thread(() -> {
int stamp = ref.getStamp();
log.debug("change A->B:{}", ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
int nowStamp = ref.getStamp();
log.debug("stamp:{}->{}", stamp, nowStamp);
}, "thread1").start();
Thread.sleep(500);
// thread2再将B修改成A
new Thread(() -> {
int stamp = ref.getStamp();
log.debug("change B->A:{}", ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
int nowStamp = ref.getStamp();
log.debug("stamp:{}->{}", stamp, nowStamp);
}, "thread2").start();

Thread.sleep(1000);
// main将A修改成C
log.debug("change A->C:{}", ref.compareAndSet(prev, "C", startStamp, startStamp + 1));
}

输出如下,发现确实无法修改,解决了ABA问题

1
2
3
4
5
6
7
10:39:03.464 [main] c.ThreadTest - main start...
10:39:03.464 [main] c.ThreadTest - 当前状态: ref:A, stamp:0
10:39:03.519 [thread1] c.ThreadTest - change A->B:true
10:39:03.519 [thread1] c.ThreadTest - stamp:0->1
10:39:04.021 [thread2] c.ThreadTest - change B->A:true
10:39:04.021 [thread2] c.ThreadTest - stamp:1->2
10:39:05.030 [main] c.ThreadTest - change A->C:false

如果我们深入到compareAndSet的方法中,可以看到实际上这种版本的机制在底层实际上相当于是多增加了一个stamp的CAS机制,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public boolean compareAndSet(V   expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}

如果我们在使用的时候不利用stamp的规则,在修改的时候不进行stamp的更新,那么也是无法解决ABA的问题的。即如果将上面的代码中,在指定expectedStamp和newStamp的时候,给定相同的值,不进行+1的操作,那么主线程还是可以完成修改。输出如下:

1
2
3
4
5
6
7
10:39:36.729 [main] c.ThreadTest - main start...
10:39:36.729 [main] c.ThreadTest - 当前状态: ref:A, stamp:0
10:39:36.776 [thread1] c.ThreadTest - change A->B:true
10:39:36.776 [thread1] c.ThreadTest - stamp:0->0
10:39:37.290 [thread2] c.ThreadTest - change B->A:true
10:39:37.290 [thread2] c.ThreadTest - stamp:0->0
10:39:38.305 [main] c.ThreadTest - change A->C:true

因此我们在使用带有版本号的原子引用的时候,需要自我约束,遵循版本stamp的使用规则

AtomicStampedReference可以给原子引用增加版本号,追踪原子引用整个的变化过程。通过它,我们可以知道引用变量在中途被更改了多少次。但是有的时候,我们可能并不关心引用变量更改了多少次,而只是单纯地关心它是否被更改过,那么可以使用AtomicMarkableReference

相比于前面利用一个整型的stamp记录版本,AtomicMarkableReference将其替换成了一个布尔型的mark,可以记载的状态就只有true和false两种了,而其他的逻辑都基本类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 构造方法
public AtomicMarkableReference(V initialRef, boolean initialMark) {
pair = Pair.of(initialRef, initialMark);
}

// cas方法
public boolean compareAndSet(V expectedReference,
V newReference,
boolean expectedMark,
boolean newMark) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedMark == current.mark &&
((newReference == current.reference &&
newMark == current.mark) ||
casPair(current, Pair.of(newReference, newMark)));
}

原子数组

原子数组类型可以保护数组内的元素的原子性,常用的原子数组类有AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray。这里简要介绍原子整数数组类的使用

构造方法:

  • public AtomicIntegerArray(int length):提供数组长度,构造一个空数组
  • public AtomicIntegerArray(int[] array):传入数组进行构造

CAS方法:

public final boolean compareAndSet(int i, int expect, int update):利用CAS修改指定下标上的值

原子更新器

原子更新器可以允许我们针对对象的某个属性进行原子操作,常用的原子更新器有AtomicReferenceFieldUpdaterAtomicIntegerFieldUpdaterAtomicLongFieldUpdater

利用原子更新器,可以针对对象的某个属性进行原子操作,但是只能配合 volatile 修饰的字段使用,否则会出现异常 IllegalArgumentException: Must be volatile type

常用 API:

  • static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> c, String fieldName):构造方法

    如果是AtomicReferenceFieldUpdater,则还需要指定一个字段的类型对应的Class类

    1
    public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)
  • abstract boolean compareAndSet(T obj, int expect, int update):CAS方法,需要指定修改哪个对象的字段


Java多线程笔记(4)-CAS与Atomic原子类
http://example.com/2022/09/11/Java多线程笔记-4-CAS与Atomic原子类/
作者
EverNorif
发布于
2022年9月11日
许可协议