为什么要有 AtomicReference ?(一)

简介: 我们之前了解过了 AtomicInteger、AtomicLong、AtomicBoolean 等原子性工具类,下面我们继续了解一下位于 java.util.concurrent.atomic 包下的工具类。

我们之前了解过了 AtomicInteger、AtomicLong、AtomicBoolean 等原子性工具类,下面我们继续了解一下位于 java.util.concurrent.atomic 包下的工具类。

关于 AtomicInteger、AtomicLong、AtomicBoolean 相关的内容请查阅

一场 Atomic XXX 的魔幻之旅

关于 AtomicReference 这种 JDK 工具类的了解的文章比较枯燥,并不是代表着文章质量的下降,因为我想搞出一整套 bestJavaer 的全方位解析,那就势必离不开对 JDK 工具类的了解。

记住:技术要做长线

AtomicReference 基本使用

我们这里再聊起老生常谈的账户问题,通过个人银行账户问题,来逐渐引入 AtomicReference 的使用,我们首先来看一下基本的个人账户类

public class BankCard {
    private final String accountName;
    private final int money;
    // 构造函数初始化 accountName 和 money
    public BankCard(String accountName,int money){
        this.accountName = accountName;
        this.money = money;
    }
    // 不提供任何修改个人账户的 set 方法,只提供 get 方法
    public String getAccountName() {
        return accountName;
    }
    public int getMoney() {
        return money;
    }
    // 重写 toString() 方法, 方便打印 BankCard
    @Override
    public String toString() {
        return "BankCard{" +
                "accountName='" + accountName + '\'' +
                ", money='" + money + '\'' +
                '}';
    }
}

个人账户类只包含两个字段:accountName 和 money,这两个字段代表账户名和账户金额,账户名和账户金额一旦设置后就不能再被修改。

现在假设有多个人分别向这个账户打款,每次存入一定数量的金额,那么理想状态下每个人在每次打款后,该账户的金额都是在不断增加的,下面我们就来验证一下这个过程。

public class BankCardTest {
    private static volatile BankCard bankCard = new BankCard("cxuan",100);
    public static void main(String[] args) {
        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                // 先读取全局的引用
                final BankCard card = bankCard;
                // 构造一个新的账户,存入一定数量的钱
                BankCard newCard = new BankCard(card.getAccountName(),card.getMoney() + 100);
                System.out.println(newCard);
                // 最后把新的账户的引用赋给原账户
                bankCard = newCard;
                try {
                    TimeUnit.MICROSECONDS.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在上面的代码中,我们首先声明了一个全局变量 BankCard,这个 BankCard 由 volatile进行修饰,目的就是在对其引用进行变化后对其他线程可见,然后每个打款人都存入一定数量的款项后,输出账户的金额变化,我们可以观察一下这个输出结果。

微信图片_20220418192149.png

可以看到,我们预想最后的结果应该是 1100 元,但是最后却只存入了 900 元,那 200 元去哪了呢?我们可以断定上面的代码不是一个线程安全的操作。

问题出现在哪里?

虽然每次 volatile 都能保证每个账户的金额都是最新的,但是由于上面的步骤中出现了组合操作,即获取账户引用更改账户引用,每个单独的操作虽然都是原子性的,但是组合在一块就不是原子性的了。所以最后的结果会出现偏差。

我们可以用如下线程切换图来表示一下这个过程的变化。

微信图片_20220418192154.png

可以看到,最后的结果可能是因为在线程 t1 获取最新账户变化后,线程切换到 t2,t2 也获取了最新账户情况,然后再切换到 t1,t1 修改引用,线程切换到 t2,t2 修改引用,所以账户引用的值被修改了两次

那么该如何确保获取引用和修改引用之间的线程安全性呢?

最简单粗暴的方式就是直接使用 synchronized 关键字进行加锁了。

使用 synchronized 保证线程安全性

使用 synchronized 可以保证共享数据的安全性,代码如下

public class BankCardSyncTest {
    private static volatile BankCard bankCard = new BankCard("cxuan",100);
    public static void main(String[] args) {
        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                synchronized (BankCardSyncTest.class) {
                    // 先读取全局的引用
                    final BankCard card = bankCard;
                    // 构造一个新的账户,存入一定数量的钱
                    BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
                    System.out.println(newCard);
                    // 最后把新的账户的引用赋给原账户
                    bankCard = newCard;
                    try {
                        TimeUnit.MICROSECONDS.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

相较于 BankCardTest ,BankCardSyncTest 增加了 synchronized 锁,运行 BankCardSyncTest 后我们发现能够得到正确的结果。

修改 BankCardSyncTest.class 为 bankCard 对象,我们发现同样能够确保线程安全性,这是因为在这段程序中,只有 bankCard 会进行变化,不会再有其他共享数据。

如果有其他共享数据的话,我们需要使用 BankCardSyncTest.clas 确保线程安全性。

除此之外,java.util.concurrent.atomic 包下的 AtomicReference 也可以保证线程安全性。

我们先来认识一下 AtomicReference ,然后再使用 AtomicReference 改写上面的代码。

相关文章
|
7月前
|
设计模式 前端开发 Java
DTO和VO的区别及使用场景详解
DTO和VO的区别及使用场景详解
4725 0
|
5月前
|
前端开发
Typora使用技巧3 —— 查看默认主题、新增自定义主题
Typora使用技巧3 —— 查看默认主题、新增自定义主题
215 0
|
2月前
|
机器学习/深度学习
YOLOv10优改系列一:YOLOv10融合C2f_Ghost网络,让YoloV10实现性能的均衡
本文介绍了YOLOv10的性能优化,通过融合Ghost模块和C2f结构,实现了网络性能的均衡。GhostNet通过GhostModule和GhostBottleNeck减少参数量,适用于资源有限的场景。YOLOv10-C2f_Ghost在减少参数和计算量的同时,保持了与原始网络相当或更好的性能。文章还提供了详细的代码修改步骤和可能遇到的问题解决方案。
262 1
YOLOv10优改系列一:YOLOv10融合C2f_Ghost网络,让YoloV10实现性能的均衡
|
7月前
|
Linux 编译器 C语言
Linux EXPORT_SYMBOL宏详解
Linux EXPORT_SYMBOL宏详解
Linux EXPORT_SYMBOL宏详解
|
7月前
|
Dragonfly 安全 算法
|
关系型数据库 MySQL Go
MySQL连接错误1045:完美解决指南
MySQL连接错误1045:完美解决指南
7074 0
|
7月前
|
Kubernetes 负载均衡 安全
ECI Pod概述
ECI能为Kubernetes提供基础的容器Pod运行环境
287 5
|
7月前
|
设计模式 缓存 Java
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理3
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
1005 1
|
缓存 Java 数据库连接
为什么不推荐使用 MyBatis 二级缓存,有哪些替代方案?
为什么不推荐使用 MyBatis 二级缓存,有哪些替代方案?
167 0
下一篇
DataWorks