原子类之原子引用

一、概述

为什么需要原子引用类型?

保证引用类型的共享变量是线程安全的。

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。

  • AtomicReference:引用类型原子类;

  • AtomicStampedRerence:原子更新带有版本号的引用类型;

    该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

  • AtomicMarkableReference :原子更新带有标记的引用类型。

    该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

二、取款示例

先做一个不使用 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
40
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
* Created by xiaowei
* Date 2022/11/5
* Description 取款案例
*/
public interface DecimalAccount {

// 获取余额
BigDecimal getBalance();

//取款
void withdraw(BigDecimal amount);

/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account){
List<Thread> threadList = new ArrayList<>();

for (int i = 0; i < 1000; i++){
threadList.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
threadList.forEach(Thread::start);
threadList.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}

不安全实现

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
import java.math.BigDecimal;

/**
* Created by xiaowei
* Date 2022/11/5
* Description 不安全实现
*/
public class Test04 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal("10000")));
}
}

class DecimalAccountUnsafe implements DecimalAccount{
BigDecimal balance;

public DecimalAccountUnsafe(BigDecimal balance){
this.balance = balance;
}

@Override
public BigDecimal getBalance() {
return balance;
}

@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance();
// 减法
this.balance = balance.subtract(amount);
}
}

运行结果

1
3850

安全实现-加锁

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
import java.math.BigDecimal;

/**
* Created by xiaowei
* Date 2022/11/5
* Description 安全实现-加锁
*/
public class Test05 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountSafeLock(new BigDecimal("10000")));
}
}

class DecimalAccountSafeLock implements DecimalAccount {
private final Object lock = new Object();
BigDecimal balance;

public DecimalAccountSafeLock(BigDecimal balance){
this.balance = balance;
}

@Override
public BigDecimal getBalance() {
return balance;
}

@Override
public void withdraw(BigDecimal amount) {
synchronized (lock) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}
}

运行结果

1
0

安全实现-CAS

AtomicReference类中,存在一个value类型的变量,保存对BigDecimal对象的引用。

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
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

/**
* Created by xiaowei
* Date 2022/11/6
* Description 安全实现-CAS
*/
public class Test06 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountSafeCas(new BigDecimal("10000")));
}
}

class DecimalAccountSafeCas implements DecimalAccount {

AtomicReference<BigDecimal> reference;

public DecimalAccountSafeCas(BigDecimal balance) {
reference = new AtomicReference<>(balance);
}

@Override
public BigDecimal getBalance() {
return reference.get();
}

@Override
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal prev = reference.get();
// 注意:这里的balance返回的是一个新的对象,即 pre!=next
BigDecimal next = prev.subtract(amount);
if (reference.compareAndSet(prev,next)) {
break;
}
}
}
}

运行结果

1
0

三、ABA 问题

3.1 概述

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

3.2 示例

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
import java.util.concurrent.atomic.AtomicReference;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/11/6
* Description ABA问题
*/
@Slf4j(topic = "c.Test07")
public class Test07 {
static AtomicReference<String> reference = new AtomicReference<>("A");

public static void main(String[] args) {
log.debug("main start...");
//获取值A
//这个共享变量被其它线程修改过?
String prev = reference.get();
other();
sleep(1);
//尝试改为C
log.debug("change A -> C {}",reference.compareAndSet(prev,"C"));
}

private static void other(){
new Thread(() -> {
log.debug("change A -> B {}",reference.compareAndSet(reference.get(),"B"));
},"t1").start();

sleep(0.5);

new Thread(() -> {
log.debug("change B -> A {}",reference.compareAndSet(reference.get(),"A"));
},"t2").start();
}
}

运行结果

1
2
3
4
16:04:35.527 [main] DEBUG c.Test07 - main start...
16:04:35.582 [t1] DEBUG c.Test07 - change A -> B true
16:04:36.084 [t2] DEBUG c.Test07 - change B -> A true
16:04:37.084 [main] DEBUG c.Test07 - change A -> C true

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况。

如果主线程希望, 只要有其它线程变动过共享变量,那么自己的 cas 就算失败,这时,需要再加一个版本号。

四、ABA问题解决

4.1 AtomicStampedReference

Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

AtomicStampedReference的compareAndSet()方法定义如下:

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)));
}

参数说明

  • expectedReference:预期引用;
  • newReference:更新后的引用;
  • expectedStamp:预期标志;
  • newStamp:更新后的标志。

如果更新后的引用和标志和当前的引用和标志相等则直接返回true,否则通过Pair生成一个新的pair对象与当前pair CAS替换。

Pair为AtomicStampedReference的内部类,主要用于记录引用和版本戳信息(标识),定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}

private volatile Pair<V> pair;

Pair记录着对象的引用和版本戳,版本戳为int型,保持自增。

同时Pair是一个不可变对象,其所有属性全部定义为final,对外提供一个of方法,该方法返回一个新建的Pari对象。pair对象定义为volatile,保证多线程环境下的可见性。在AtomicStampedReference中,大多方法都是通过调用Pair的of方法来产生一个新的Pair对象,然后赋值给变量pair。

如set方法:

1
2
3
4
5
public void set(V newReference, int newStamp) {
Pair<V> current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}

4.2 使用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
30
31
32
33
34
35
36
37
38
39
40
41
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicStampedReference;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/11/6
* Description AtomicStampedReference
*/
@Slf4j(topic = "c.Test08")
public class Test08 {
static AtomicStampedReference<String> reference = new AtomicStampedReference<>("A",0);

public static void main(String[] args) {
log.debug("main start...");
//获取值 A
String prev = reference.getReference();
// 获取版本号
int stamp = reference.getStamp();
log.debug("版本{}",stamp);
// 如果中间有其它线程干扰,发生了ABA现象
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", reference.compareAndSet(prev, "C", stamp, stamp + 1));
}

private static void other() {
new Thread(() -> {
log.debug("change A->B {}", reference.compareAndSet(reference.getReference(), "B", reference.getStamp(), reference.getStamp() + 1));
log.debug("更新版本为 {}", reference.getStamp());
},"t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", reference.compareAndSet(reference.getReference(), "A", reference.getStamp(), reference.getStamp() + 1));
log.debug("更新版本为 {}", reference.getStamp());
}, "t2").start();
}
}

运行结果

1
2
3
4
5
6
7
16:36:35.347 [main] DEBUG c.Test08 - main start...
16:36:35.349 [main] DEBUG c.Test08 - 版本0
16:36:35.403 [t1] DEBUG c.Test08 - change A->B true
16:36:35.404 [t1] DEBUG c.Test08 - 更新版本为 1
16:36:35.905 [t2] DEBUG c.Test08 - change B->A true
16:36:35.905 [t2] DEBUG c.Test08 - 更新版本为 2
16:36:36.905 [main] DEBUG c.Test08 - change A->C false

分析

main线程最初的版本号是0,main线程在修改值之前,已经被其他线程修改了2个版本,等到main线程修改时发现版本号不一致了,所以修改值失败。

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

4.3 AtomicMarkableReference解决ABA问题

有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference

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
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicMarkableReference;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/11/6
* Description AtomicMarkableReference解决ABA问题
*/
@Slf4j(topic = "c.TestAtomicMarkableReference")
public class TestAtomicMarkableReference {
static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);

public static void main(String[] args){
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
log.debug("是否被改过 {}", ref.isMarked());
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C
log.debug("能否 change A->C {}", ref.compareAndSet(prev, "C", false, true));
}

private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", false, true));
log.debug("修改A->B {}", ref.isMarked());
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", true, true));
log.debug("修改B->A {}", ref.isMarked());
}, "t1").start();
}
}

运行结果

1
2
3
4
5
6
7
16:40:25.278 [main] DEBUG c.TestAtomicMarkableReference - main start...
16:40:25.280 [main] DEBUG c.TestAtomicMarkableReference - 是否被改过 false
16:40:25.336 [t1] DEBUG c.TestAtomicMarkableReference - change A->B true
16:40:25.337 [t1] DEBUG c.TestAtomicMarkableReference - 修改A->B true
16:40:25.337 [t1] DEBUG c.TestAtomicMarkableReference - change B->A true
16:40:25.337 [t1] DEBUG c.TestAtomicMarkableReference - 修改B->A true
16:40:26.337 [main] DEBUG c.TestAtomicMarkableReference - 能否 change A->C false

原子类之原子引用
http://example.com/2023/03/29/原子类之原子引用/
作者
程序员小魏
发布于
2023年3月29日
许可协议