为什么要有 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 改写上面的代码。

相关文章
|
12月前
|
机器学习/深度学习
YOLOv10优改系列一:YOLOv10融合C2f_Ghost网络,让YoloV10实现性能的均衡
本文介绍了YOLOv10的性能优化,通过融合Ghost模块和C2f结构,实现了网络性能的均衡。GhostNet通过GhostModule和GhostBottleNeck减少参数量,适用于资源有限的场景。YOLOv10-C2f_Ghost在减少参数和计算量的同时,保持了与原始网络相当或更好的性能。文章还提供了详细的代码修改步骤和可能遇到的问题解决方案。
1457 1
YOLOv10优改系列一:YOLOv10融合C2f_Ghost网络,让YoloV10实现性能的均衡
|
Linux 对象存储 Windows
MinIO 客户端安装与使用教程
详细讲解MinIO CLI的安装与使用
4549 0
|
Kubernetes 负载均衡 安全
ECI Pod概述
ECI能为Kubernetes提供基础的容器Pod运行环境
543 5
|
关系型数据库 MySQL Go
MySQL连接错误1045:完美解决指南
MySQL连接错误1045:完美解决指南
11318 0
|
关系型数据库 MySQL
【MySQL进阶之路 | 基础篇】约束之CHECK约束与DEFAULT约束
【MySQL进阶之路 | 基础篇】约束之CHECK约束与DEFAULT约束
|
数据安全/隐私保护
Internal error XFS_WANT_CORRUPTED_GOTO at line 1635 of file fs/xfs/libxfs/xfs_alloc.c.
下面为解决问题中报的错误: Internal error XFS_WANT_CORRUPTED_GOTO at line 1635 of file fs/xfs/libxfs/xfs_alloc.c. Caller xfs_free_extent
756 0
Internal error XFS_WANT_CORRUPTED_GOTO at line 1635 of file fs/xfs/libxfs/xfs_alloc.c.
|
Java 关系型数据库 MySQL
java.sql.SQLException: No operations allowed after statement closed.
java.sql.SQLException: No operations allowed after statement closed. 原因很简单:这里和数据库的连接Connection是一个Static的,程序共享这一个Connection。
18062 0
|
前端开发 JavaScript IDE
创建 SpringBoot 项目的 3 种方式
创建 SpringBoot 项目的 3 种方式
2702 0
创建 SpringBoot 项目的 3 种方式
|
Java 数据安全/隐私保护
SpringMVC+Spring+Mybatis实现登录注册Demo
SpringMVC+Spring+Mybatis实现登录注册Demo
387 0
SpringMVC+Spring+Mybatis实现登录注册Demo
|
3天前
|
存储 弹性计算 人工智能
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
2025年9月24日,阿里云弹性计算团队多位产品、技术专家及服务器团队技术专家共同在【2025云栖大会】现场带来了《通用计算产品发布与行业实践》的专场论坛,本论坛聚焦弹性计算多款通用算力产品发布。同时,ECS云服务器安全能力、资源售卖模式、计算AI助手等用户体验关键环节也宣布升级,让用云更简单、更智能。海尔三翼鸟云服务负责人刘建锋先生作为特邀嘉宾,莅临现场分享了关于阿里云ECS g9i推动AIoT平台的场景落地实践。
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾