为何会有 AtomicReference ?

我把本身以往的文章汇总成为了 Github ,欢迎各位大佬 star
https://github.com/crisxuan/bestJavaerjava

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

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

一场 Atomic XXX 的魔幻之旅github

关于 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 改写上面的代码。

了解 AtomicReference

使用 AtomicReference 保证线程安全性

下面咱们改写一下上面的那个示例

public class BankCardARTest {

    private static AtomicReference<BankCard> bankCardRef = new AtomicReference<>(new BankCard("cxuan",100));

    public static void main(String[] args) {

        for(int i = 0;i < 10;i++){
            new Thread(() -> {
                while (true){
                    // 使用 AtomicReference.get 获取
                    final BankCard card = bankCardRef.get();
                    BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
                    // 使用 CAS 乐观锁进行非阻塞更新
                    if(bankCardRef.compareAndSet(card,newCard)){
                        System.out.println(newCard);
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

在上面的示例代码中,咱们使用了 AtomicReference 封装了 BankCard 的引用,而后使用 get() 方法得到原子性的引用,接着使用 CAS 乐观锁进行非阻塞更新,更新的标准是若是使用 bankCardRef.get() 获取的值等于内存值的话,就会把银行卡帐户的资金 + 100,咱们观察一下输出结果。

能够看到,有一些输出是乱序执行的,出现这个缘由很简单,有可能在输出结果以前,进行线程切换,而后打印了后面线程的值,而后线程切换回来再进行输出,可是能够看到,没有出现银行卡金额相同的状况。

AtomicReference 源码解析

在了解上面这个例子以后,咱们来看一下 AtomicReference 的使用方法

AtomicReference 和 AtomicInteger 很是类似,它们内部都是用了下面三个属性

wTiyJH.png

Unsafesun.misc 包下面的类,AtomicReference 主要是依赖于 sun.misc.Unsafe 提供的一些 native 方法保证操做的原子性

Unsafe 的 objectFieldOffset 方法能够获取成员属性在内存中的地址相对于对象内存地址的偏移量。这个偏移量也就是 valueOffset ,说得简单点就是找到这个变量在内存中的地址,便于后续经过内存地址直接进行操做。

value 就是 AtomicReference 中的实际值,由于有 volatile ,这个值实际上就是内存值。

不一样之处就在于 AtomicInteger 是对整数的封装,而 AtomicReference 则对应普通的对象引用。也就是它能够保证你在修改对象引用时的线程安全性。

get and set

咱们首先来看一下最简单的 get 、set 方法:

get() : 获取当前 AtomicReference 的值

set() : 设置当前 AtomicReference 的值

get() 能够原子性的读取 AtomicReference 中的数据,set() 能够原子性的设置当前的值,由于 get() 和 set() 最终都是做用于 value 变量,而 value 是由 volatile 修饰的,因此 get 、set 至关于都是对内存进行读取和设置。以下图所示

lazySet 方法

volatile 有内存屏障你知道吗?

内存屏障是啥啊?

内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操做中的一个同步点,使得此点以前的全部读写操做都执行后才能够开始执行此点以后的操做。也是一个让CPU 处理单元中的内存状态对其它处理单元可见的一项技术。

CPU 使用了不少优化,使用缓存、指令重排等,其最终的目的都是为了性能,也就是说,当一个程序执行时,只要最终的结果是同样的,指令是否被重排并不重要。因此指令的执行时序并非顺序执行的,而是乱序执行的,这就会带来不少问题,这也促使着内存屏障的出现。

语义上,内存屏障以前的全部写操做都要写入内存;内存屏障以后的读操做均可以得到同步屏障以前的写操做的结果。所以,对于敏感的程序块,写操做以后、读操做以前能够插入内存屏障。

内存屏障的开销很是轻量级,可是再小也是有开销的,LazySet 的做用正是如此,它会以普通变量的形式来读写变量。

也能够说是:懒得设置屏障了

getAndSet 方法

以原子方式设置为给定值并返回旧值。它的源码以下

它会调用 unsafe 中的 getAndSetObject 方法,源码以下

能够看到这个 getAndSet 方法涉及两个 cpp 实现的方法,一个是 getObjectVolatile ,一个是 compareAndSwapObject 方法,他们用在 do...while 循环中,也就是说,每次都会先获取最新对象引用的值,若是使用 CAS 成功交换两个对象的话,就会直接返回 var5 的值,var5 此时应该就是更新前的内存值,也就是旧值。

compareAndSet 方法

这就是 AtomicReference 很是关键的 CAS 方法了,与 AtomicInteger 不一样的是,AtomicReference 是调用的 compareAndSwapObject ,而 AtomicInteger 调用的是 compareAndSwapInt 方法。这两个方法的实现以下

路径在 hotspot/src/share/vm/prims/unsafe.cpp 中。

咱们以前解析过 AtomicInteger 的源码,因此咱们接下来解析一下 AtomicReference 源码。

由于对象存在于堆中,因此方法 index_oop_from_field_offset_long 应该是获取对象的内存地址,而后使用 atomic_compare_exchange_oop 方法进行对象的 CAS 交换。

这段代码会首先判断是否使用了 UseCompressedOops,也就是指针压缩

这里简单解释一下指针压缩的概念:JVM 最初的时候是 32 位的,可是随着 64 位 JVM 的兴起,也带来一个问题,内存占用空间更大了 ,可是 JVM 内存最好不要超过 32 G,为了节省空间,在 JDK 1.6 的版本后,咱们在 64位中的 JVM 中能够开启指针压缩(UseCompressedOops)来压缩咱们对象指针的大小,来帮助咱们节省内存空间,在 JDK 8来讲,这个指令是默认开启的。

若是不开启指针压缩的话,64 位 JVM 会采用 8 字节(64位)存储真实内存地址,比以前采用4字节(32位)压缩存储地址带来的问题:

  1. 增长了 GC 开销:64 位对象引用须要占用更多的堆空间,留给其余数据的空间将会减小,
    从而加快了 GC 的发生,更频繁的进行 GC。
  2. 下降 CPU 缓存命中率:64 位对象引用增大了,CPU 能缓存的 oop 将会更少,从而下降了 CPU 缓存的效率。

因为 64 位存储内存地址会带来这么多问题,程序员发明了指针压缩技术,可让咱们既可以使用以前 4 字节存储指针地址,又可以扩大内存存储。

能够看到,atomic_compare_exchange_oop 方法底层也是使用了 Atomic:cmpxchg 方法进行 CAS 交换,而后把旧值进行 decode 返回 (我这局限的 C++ 知识,只能解析到这里了,若是你们懂这段代码必定告诉我,让我请教一波)

weakCompareAndSet 方法

weakCompareAndSet: 很是认真看了好几遍,发现 JDK1.8 的这个方法和 compareAndSet 方法彻底一摸同样啊,坑我。。。

可是真的是这样么?并非,JDK 源码很博大精深,才不会设计一个重复的方法,你想一想 JDK 团队也不是会犯这种低级团队,可是缘由是什么呢?

《Java 高并发详解》这本书给出了咱们一个答案

总结

此篇文章主要介绍了 AtomicReference 的出现背景,AtomicReference 的使用场景,以及介绍了 AtomicReference 的源码,重点方法的源码分析。此篇 AtomicReference 的文章基本上涵盖了网络上全部关于 AtomicReference 的内容了,遗憾的是就是 cpp 源码可能分析的不是很到位,这须要充足的 C/C++ 编程知识,若是有读者朋友们有最新的研究成果,请及时告诉我。

另外,添加个人微信 becomecxuan,加入每日一题群,天天一道面试题分享,更多内容请参见个人 Github,成为最好的 bestJavaer,已经收录此篇文章,详情见原文连接

我本身肝了六本 PDF,微信搜索「程序员cxuan」关注公众号后,在后台回复 cxuan ,领取所有 PDF,这些 PDF 以下

六本 PDF 连接

相关文章
相关标签/搜索