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

            </div>
目录
相关文章
|
Linux 图形学 Android开发
5款好用的工具软件推荐给你
人类与99%的动物之间最大差别在于是否会运用工具,借助好的工具,能提升几倍的工作效率。
139 0
|
关系型数据库 MySQL Windows
MySQL启动时显示:发生系统错误 5。拒绝访问解决办法
MySQL启动时显示:发生系统错误 5。拒绝访问解决办法
MySQL启动时显示:发生系统错误 5。拒绝访问解决办法
|
5月前
|
API
Flutter 解决Type ‘MouseCursor‘ not found.
Flutter 解决Type ‘MouseCursor‘ not found.
57 0
|
7月前
|
搜索推荐 算法 数据挖掘
十大排序算法详解-上篇:比较排序算法【python 动态图解】
十大排序算法详解-上篇:比较排序算法【python 动态图解】
|
机器学习/深度学习 安全 算法
对程序员来说,有价值的工作
对程序员来说,有价值的工作
274 1
|
数据挖掘 Java 索引
【Nowcoder牛客刷题记录】——Python数据分析(一)
【Nowcoder牛客刷题记录】——Python数据分析(一)
735 1
|
前端开发 Shell 开发工具
对已经提交到公共仓库中的 commit 进行修改
开发过程中可能遇到这么一种常见的场景,敲完代码,开心的提交到远程仓库了,在欣赏着自己优美代码的时候,发现commit的信息中有个错别字,这是有着代码洁癖的你绝对不能忍的。那么如何取修改已经提交到远程仓库的commit呢。
195 0
【TP5.1】Facade的使用
【TP5.1】Facade的使用
160 0
【TP5.1】Facade的使用
|
SQL 关系型数据库 MySQL
order by使用
order by使用
165 0
order by使用
|
缓存 Java 应用服务中间件
高可用后端架构设计实战-利用request cache请求缓存优化批量查询接口
高可用后端架构设计实战-利用request cache请求缓存优化批量查询接口
253 0
高可用后端架构设计实战-利用request cache请求缓存优化批量查询接口