一、概述
为什么需要原子引用类型?
保证引用类型的共享变量是线程安全的。
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。
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;
public interface DecimalAccount {
BigDecimal getBalance();
void withdraw(BigDecimal amount);
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;
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 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;
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); } } }
|
运行结果
安全实现-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;
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(); BigDecimal next = prev.subtract(amount); if (reference.compareAndSet(prev,next)) { break; } } } }
|
运行结果
三、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;
@Slf4j(topic = "c.Test07") public class Test07 { static AtomicReference<String> reference = new AtomicReference<>("A");
public static void main(String[] args) { log.debug("main start..."); String prev = reference.get(); other(); sleep(1); 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;
@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..."); String prev = reference.getReference(); int stamp = reference.getStamp(); log.debug("版本{}",stamp); other(); sleep(1); 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;
@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..."); String prev = ref.getReference(); log.debug("是否被改过 {}", ref.isMarked()); other(); sleep(1); 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
|