为什么要有 AtomicReference ?(二)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 我们之前了解过了 AtomicInteger、AtomicLong、AtomicBoolean 等原子性工具类,下面我们继续了解一下位于 java.util.concurrent.atomic 包下的工具类。

了解 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,我们观察一下输出结果。

微信图片_20220418192251.png

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

AtomicReference 源码解析

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

AtomicReference 和 AtomicInteger 非常相似,它们内部都是用了下面三个属性

微信图片_20220418192256.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 方法

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

微信图片_20220418192302.png

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

微信图片_20220418192305.png

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

compareAndSet 方法

这就是 AtomicReference 非常关键的 CAS 方法了,与 AtomicInteger 不同的是,AtomicReference 是调用的 compareAndSwapObject ,而 AtomicInteger 调用的是 compareAndSwapInt 方法。这两个方法的实现如下

微信图片_20220418192312.png

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

我们之前解析过 AtomicInteger 的源码,所以我们接下来解析一下 AtomicReference 源码。

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

微信图片_20220418192316.png

这段代码会首先判断是否使用了 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 高并发详解》这本书给出了我们一个答案。

微信图片_20220418192322.png

总结

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


            </div>
目录
相关文章
|
7月前
|
NoSQL 安全 Java
Spring Boot3整合Redis
Spring Boot3整合Redis
226 1
|
7月前
|
JSON Java API
GSON 泛型对象反序列化解决方案
GSON 泛型对象反序列化解决方案
332 0
|
Linux
Linux大文件查看利器:掌握Less命令的使用和技巧
Linux大文件查看利器:掌握Less命令的使用和技巧
1089 0
|
开发框架 Java 数据库
java----包的命名规范
对包的解释与命名规则
9127 0
java----包的命名规范
|
5月前
|
JavaScript 搜索推荐
js 混合排序(同时存在数字、字母、汉字等)
js 混合排序(同时存在数字、字母、汉字等)
281 0
|
安全 Windows 编解码
怎么设置服务器禁止被ping
怎么设置服务器禁止被ping 如何禁止服务器被ping--怎么设置:频繁地使用Ping命令会导致网络堵塞、降低传输效率,为了避免恶意的网络攻击,一般都会拒绝用户Ping服务器。为实现这一目的,不仅可以在防火墙中进行设置,也可以在路由器上进行设置,并且还可以利用Windows2000/2003系统自身的功能实现。
5971 0
|
前端开发 Java API
异步编程 - 11 Spring WebFlux的异步非阻塞处理2
异步编程 - 11 Spring WebFlux的异步非阻塞处理2
172 0
|
前端开发 Java API
异步编程 - 11 Spring WebFlux的异步非阻塞处理
异步编程 - 11 Spring WebFlux的异步非阻塞处理
474 0
|
存储 Java 中间件
SpringBoot+MDC链路追踪trace_id丢失
前言 trace_id是用来标识同一个请求的唯一标识,不管请求经过多少服务,都可以通过tracid排查对应模块的日志信息找到对应请求的一些细节,是排查问题的一个重要线索。
475 0
|
NoSQL Java Scala
Flink - The object probably contains or references non serializable fields 无法序列化问题
使用 Flink 自定义 Source 生成数据时,集群提交任务时显示 org.apache.log4j.Logger@72c927f1 is not serializable. The object probably contains or references non serializable fields.
1427 0
Flink - The object probably contains or references non serializable fields 无法序列化问题