我们之前了解过了 AtomicInteger、AtomicLong、AtomicBoolean 等原子性工具类,下面我们继续了解一下位于 java.util.concurrent.atomic
包下的工具类。
关于 AtomicInteger、AtomicLong、AtomicBoolean 相关的内容请查阅
关于 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
进行修饰,目的就是在对其引用进行变化后对其他线程可见,然后每个打款人都存入一定数量的款项后,输出账户的金额变化,我们可以观察一下这个输出结果。
可以看到,我们预想最后的结果应该是 1100 元,但是最后却只存入了 900 元,那 200 元去哪了呢?我们可以断定上面的代码不是一个线程安全的操作。
问题出现在哪里?
虽然每次 volatile 都能保证每个账户的金额都是最新的,但是由于上面的步骤中出现了组合操作,即获取账户引用
和更改账户引用
,每个单独的操作虽然都是原子性的,但是组合在一块就不是原子性的了。所以最后的结果会出现偏差。
我们可以用如下线程切换图来表示一下这个过程的变化。
可以看到,最后的结果可能是因为在线程 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 改写上面的代码。