李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
34.并发编程之原子引用
Leefs
2022-11-06 PM
1224℃
0条
[TOC] ### 一、概述 **为什么需要原子引用类型?** 保证引用类型的共享变量是线程安全的。 基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。 - `AtomicReference`:引用类型原子类; - `AtomicStampedRerence`:原子更新带有版本号的引用类型; 该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - `AtomicMarkableReference` :原子更新带有标记的引用类型。 该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 ### 二、取款示例 > 先做一个不使用 **AtomicReference** 取款的不安全实现 + **取款案例** ```java import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; /** * Created by lilinchao * Date 2022/11/5 * Description 取款案例 */ public interface DecimalAccount { // 获取余额 BigDecimal getBalance(); //取款 void withdraw(BigDecimal amount); /** * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(DecimalAccount account){ List
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()); } } ``` #### 不安全实现 ```java import java.math.BigDecimal; /** * Created by lilinchao * 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); } } ``` **运行结果** ``` 3850 ``` #### 安全实现-加锁 ```java import java.math.BigDecimal; /** * Created by lilinchao * 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); } } } ``` **运行结果** ``` 0 ``` #### 安全实现-CAS 在`AtomicReference`类中,存在一个value类型的变量,保存对`BigDecimal`对象的引用。 ```java import java.math.BigDecimal; import java.util.concurrent.atomic.AtomicReference; /** * Created by lilinchao * 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
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; } } } } ``` **运行结果** ``` 0 ``` ### 三、ABA 问题 #### 3.1 概述 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。 #### 3.2 示例 ```java import java.util.concurrent.atomic.AtomicReference; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/11/6 * Description ABA问题 */ @Slf4j(topic = "c.Test07") public class Test07 { static AtomicReference
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(); } } ``` **运行结果** ``` 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()方法定义如下: ```java public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair
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`的内部类,主要用于记录引用和版本戳信息(标识),定义如下: ```java private static class Pair
{ final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static
Pair
of(T reference, int stamp) { return new Pair
(reference, stamp); } } private volatile Pair
pair; ``` Pair记录着对象的引用和版本戳,版本戳为int型,保持自增。 同时Pair是一个不可变对象,其所有属性全部定义为final,对外提供一个of方法,该方法返回一个新建的Pari对象。pair对象定义为volatile,保证多线程环境下的可见性。在`AtomicStampedReference`中,大多方法都是通过调用Pair的of方法来产生一个新的Pair对象,然后赋值给变量pair。 **如set方法:** ```java public void set(V newReference, int newStamp) { Pair
current = pair; if (newReference != current.reference || newStamp != current.stamp) this.pair = Pair.of(newReference, newStamp); } ``` #### 4.2 使用AtomicStampedReference解决ABA问题 ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.atomic.AtomicStampedReference; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/11/6 * Description AtomicStampedReference */ @Slf4j(topic = "c.Test08") public class Test08 { static AtomicStampedReference
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(); } } ``` **运行结果** ``` 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` ```java import lombok.extern.slf4j.Slf4j; import java.util.concurrent.atomic.AtomicMarkableReference; import static com.lilinchao.concurrent.utils.Sleeper.sleep; /** * Created by lilinchao * Date 2022/11/6 * Description AtomicMarkableReference解决ABA问题 */ @Slf4j(topic = "c.TestAtomicMarkableReference") public class TestAtomicMarkableReference { static AtomicMarkableReference
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(); } } ``` **运行结果** ``` 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 ``` *附参考原文地址* *《黑马程序员之并发编程》*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2552.html
上一篇
33.AtomicInteger原子整数
下一篇
35.并发编程之原子数组
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
Spark RDD
RSA加解密
Sentinel
Jquery
JVM
HDFS
Elastisearch
GET和POST
JavaScript
设计模式
人工智能
Tomcat
随笔
Spark
Flink
持有对象
Stream流
Java编程思想
Spark Streaming
Yarn
Linux
微服务
Eclipse
Golang
排序
VUE
Spark Core
Java工具类
SpringCloud
数学
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭