图解java.util.concurrent并发包源码系列,原子类、CAS、AtomicLong、AtomicStampedReference一套带走

news2024/7/5 0:10:07

图解java.util.concurrent并发包源码系列,原子类、CAS、AtomicLong、AtomicStampedReference一套带走

  • 原子类
    • 为什么要使用原子类
    • CAS
  • AtomicLong源码解析
  • AtomicLong的问题
    • ABA问题
      • AtomicStampedReference
    • 高并发情况下大量的CAS失败,导致CPU空转

往期文章:

  • 人人都能看懂的图解java.util.concurrent并发包源码系列 ThreadPoolExecutor线程池

原子类

java.util.concurrent.atomic包中有各种各样的原子类,比如AtomicInteger、AtomicLong、AtomicLongArray、AtomicReference、AtomicStampedReference、LongAdder等,它们提供了对不同类型的变量的原子操作

为什么要使用原子类

如果我们对一个方法内部的局部变量做操作,比如自增自减,那是不需要使用原子类的,因为此时操作该变量的只有一个线程,是线程安全的,对该变量的操作得到的结果必然与我们预期的结果一致。

public class Test {
    public static void numAdd100() {
        int num = 0;
        for (int i = 0; i < 100; i++) {
            num++;
        }
        // 打印的必然是100
        System.out.println(num);
    }

    public static void main(String[] args) {
        numAdd100();
    }
}

但是我们对一个成员变量做自增自减操作呢?如果还是只有一个线程,那得到的结果还是与我们预期的结果是一致的,但是如果有多个线程同时操作这个变量,那么得到的结果就不一定和我们预期的结果一致了。

public class Test {
    private static int num;

    public static void addNum100() {
        for (int i = 0; i < 100; i++) {
            num++;
        }
    }

    public static int getNum() {
        return num;
    }

    public static void main(String[] args) throws InterruptedException {
        // 100个线程,每个线程都对num变量加100
        for (int i = 0; i < 100; i++) {
            new Thread(() -> Test.addNum100()).start();
        }
        Thread.sleep(1000);
        // 得到的结果就不一定的10000了
        System.out.println(Test.getNum());
    }
}

为什么会出现不一致呢?那是因为 num++ 这个操作不是原子的,它分成三步:获取变量num的值,对num的值加1,加1后的值写回num变量

在这里插入图片描述

那如果同一时刻,有多个线程做这个 num++ 的操作,会出现什么情况呢?

在这里插入图片描述

可以看到,加了两次,但是最后只有一次的结果,有一次的加1操作丢了,这样就出现了不一致。

造成这种现象的根本原因,其实就是因为我们对num这个变量没有任何保护措施,任何线程过来都可以随意对它进行操作。如果我们要避免上面的这种情况发生,就要对num这个变量添加一定的保护措施。

比如我们可以使用synchronized关键字,以加锁的方式对该变量进行操作,这样同一时刻就只有一个线程对num变量进行操作,其他获取不到锁的线程就要在锁池中进行等待。

public class Test {

    private static Object object = new Object();

    private static int num;

    public static void addNum1() {
        synchronized (object) {
            num++;
        }
    }

    public static int getNum() {
        return num;
    }

    public static void main(String[] args) throws InterruptedException {
        // 100个线程,每个线程都对num变量加100
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    Test.addNum1();
                }
            }).start();
        }
        Thread.sleep(1000);
        // 得到的结果一定是10000
        System.out.println(Test.getNum());
    }
}

在这里插入图片描述

但是加synchronized锁这种操作是非常重的,它需要通过系统调用请求操作系统加互斥锁Mutex,性能会非常低下,因此还有另外一种方式,那就是使用原子类。

public class Test {
    private static AtomicInteger num = new AtomicInteger();

    public static void addNum100() {
        for (int i = 0; i < 100; i++) {
            num.incrementAndGet();
        }
    }

    public static int getNum() {
        return num.get();
    }

    public static void main(String[] args) throws InterruptedException {
        // 100个线程,每个线程都对num变量加100
        for (int i = 0; i < 100; i++) {
            new Thread(() -> Test.addNum100()).start();
        }
        Thread.sleep(1000);
        // 得到的结果就一定是10000
        System.out.println(Test.getNum());
    }
}

为什么使用了原子类可以有这种效果呢?那就是因为它内部使用了 自旋+CAS 的操作。

CAS

CAS就是比较并交换的意思(Compare And Swap)。在给一个变量赋值之前,会先判断这个变量的值是否和我们预期的一致,如果一致,则表示中间没有人修改过,那么就可以把变量设置为我们传递的值,如果变量的值和我们预期的不一致,表示中间有人修改过,那么就不设置为我们传递的值。

在这里插入图片描述

然后如果设置失败,我们可以通过自旋进行重试。我们可以获取到变量最新的值,我们更新预期的值为变量最新的值,再次进行CAS操作,直到成功为止,这就是自旋操作。CAS一般都会搭配自旋一起使用。

在这里插入图片描述

这样会有什么好处呢?那就是CAS失败的线程不用挂起,可以通过自旋进行重试,直到成功为止,这样性能会比加互斥锁要高

事实上AtomicInteger、AtomicLong等原子类,使用的就是这样方式。

AtomicLong源码解析

我们来看看AtomicLong里面的源码,验证上面所说的那种自旋+CAS 的思想是否正确。

public class AtomicLong extends Number implements java.io.Serializable {
    
    // Unsafe类,可以通过这个类做一些更底层的操作,比如分配释放内存,调用底层CAS操作等
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
	// value变量的内存偏移量
    private static final long valueOffset;
	
	static {
        try {
        	// 通过unsafe对象初始化value变量的内存偏移量valueOffset 
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

	// value变量,volatile变量保证内存可见性
	private volatile long value;

	// 获取value变量的值
	public final long get() {
        return value;
    }
    
    // 通过unsafe类进行自旋+CAS
    public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
	
}

首先解析这一句:

    // Unsafe类,可以通过这个类做一些更底层的操作,比如分配释放内存,调用底层CAS操作等
    private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe类,可以通过这个类做一些更底层的操作,我们以内存分配和释放为例。C语言是可以通过malloc函数分配指定大小的内存,然后可以通过free函数释放内存,也就是C语言可以灵活的操作内存的分配和释放,而Java则不可以,只能new一个对象。但是我们可以通过Unsafe类,来分配和释放内存,Unsafe类提供了allocateMemory方法可以用于内存的分配,freeMemory方法进行内存的释放,它们都是native方法,会调用到底层的C++代码。


public native long allocateMemory(long var1);

public native void freeMemory(long var1);

在这里插入图片描述

那AtomicLong拿Unsafe类来做什么呢?就是调用Unsafe提供的compareAndSwapLong方法进行原子的CAS操作。如果我们自己写代码实现CAS的话,是无法保证原子性的,比如一个if判断变量是否符合预期,符合则赋值,这样一看就知道是非原子性的操作。

然后再来看这一段:

	// value变量的内存偏移量
    private static final long valueOffset;
	
	static {
        try {
        	// 通过unsafe对象初始化value变量的内存偏移量valueOffset 
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

这一段的大体意思就是在初始化AtomicLong时就通过静态代码块,调用unsafe的objectFieldOffset方法拿到AtomicLong的value变量的内存偏移量,保存到valueOffset变量中。之所以要这么做,是因为当调用unsafe的compareAndSwapLong方法时需要提供value变量的内存偏移量。

在这里插入图片描述

然后再看这一句:

	// value变量,volatile变量保证内存可见性
	private volatile long value;

这个就是AtomicLong里面的value变量,通过volatile关键字修饰,保证它的内存可见性,一个线程修改了它的值,其他线程可以马上看到最新值。

然后再往下看:

    // 通过unsafe类进行自旋+CAS
    public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }

AtomicLong的getAndIncrement调用了unsafe的getAndAddLong方法,传递了valueOffset参数,我们进入unsafe的getAndAddLong方法看看:

	/**
	* 参数:
	* var1:AtomicLong对象
	* var2:valueOffset
	* var4:1L
	*/
    public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
        	// 调用了Unsafe的getLongVolatile获取value变量的值,赋值到var6,参数是AtomicLong对象和valueOffset
            var6 = this.getLongVolatile(var1, var2);
          // 调用了Unsafe的compareAndSwapLong方法进行CAS操作修改value变量,如果成功则退出while循环,不成功则自旋重试
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

可以看到是完全符合我们上面所说的自旋+CAS的思想。首先通过Unsafe的getLongVolatile方法,根据当前AtomicLong的内存地址和valueOffset内存偏移量,获取指定内存区域上的value值,然后通过Unsafe的compareAndSwapLong方法进行底层的CAS操作修改value值
在这里插入图片描述
getLongVolatile和compareAndSwapLong都是native方法,会调用底层的C++代码。


public native long getLongVolatile(Object var1, long var2);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

然后底层C++的代码会调用 “Lock cmpxchg” 汇编指令,这个汇编指令会通过底层CPU提供的CAS指令进行CAS操作,提供硬件级别的原子性保障

在这里插入图片描述
最后来一张大图对AtomicLong的源码做个总结:

在这里插入图片描述

AtomicLong的问题

上面介绍了AtomicLong的好处,但是AtomicLong也有一些明显的问题:

  • ABA问题
  • 高并发情况下大量的CAS失败,导致CPU空转

ABA问题

假设现在有两个线程,我们称为1号线程和2号线程,然后内存中有一个变量value。1号线程首先把value设置为A,然后2线程通过CAS设置为B,随后又通过CAS设置为A。然后此时1号线程过来要执行CAS操作,发现value仍然是A,它就以为value没有被修改过,而实际上value是已经被人修改过它,而它却无法感知到。这就是ABA问题。

在这里插入图片描述

那如何避免这种问题呢?一种方法是保证value修改时顺序递增的,不允许把值改回去;另一种方法就是增加一个版本号或者时间戳来记录value修改的情况,AtomicStampedReference就是这种方法的实现

AtomicStampedReference

AtomicStampedReference提供了CAS原子更新一整个对象的功能,是AtomicReference的加强版,在AtomicReference的基础上增加了stamp版本号机制解决了ABA问题

以下就是AtomicStampedReference的核心源码:

public class AtomicStampedReference<V> {

	// 用一个Pair对象包装了我们的对象引用和stamp版本号
    private static class Pair<T> {
    	// 我们的对象
        final T reference;
        // 版本号
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

	// pair对象,包装了我们的对象引用和stamp版本号
    private volatile Pair<V> pair;

	// 获取我们的对象reference
	public V getReference() {
        return pair.reference;
    }

	// 获取当前版本号
	public int getStamp() {
        return pair.stamp;
    }

	// 通过Unsafe类的objectFieldOffset方法获取pair的内存偏移量pairOffset
	private static final long pairOffset =
        	objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
	
	/*
	* CAS更新reference
	* expectedReference:预期值
	* newReference:reference与预期值expectedReference匹配,并且版本号也与预期的匹配,则更新为newReference
	* expectedStamp:预期版本号
	* newStamp:新的版本号,如果版本号stamp也与预期版本号expectedStamp匹配,则stamp更新为newStamp
	*/
	public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference && // reference与预期值expectedReference匹配
            expectedStamp == current.stamp && // 版本号stamp与预期版本号expectedStamp匹配
            ((newReference == current.reference &&
              newStamp == current.stamp) || // 版本号没变,就不更新了
             casPair(current, Pair.of(newReference, newStamp))); // CAS更新pair对象
    }

	// 调用Unsafe的compareAndSwapObject进行CAS操作更新pair对象
	private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
}

可以看到原理大体和AtomicLong、AtomicReference等相差不远,区别就是增加了一个stamp版本号,并且把需要原子性保障的对象值reference和版本号stamp包装成一个pair对象,外面的内存偏移量是pair对象在AtomicStampReference中的内存偏移量,CAS原子更新的是一整个pair对象。我们只要保证stamp是顺序递增的,就不会出现ABA问题,而reference可以随意修改,不需要保证顺序递增的语义。

在这里插入图片描述

高并发情况下大量的CAS失败,导致CPU空转

因为自旋+CAS这种机制,如果CAS失败是要自旋重试,如果在高并发情况下,许多线程同时进行CAS操作,只会有一个线程CAS成功,其他线程就要自旋重试,者就会有大量的线程在那里进行while循环,但是啥也没干,而此时CPU使用率却达到100%,者就严重浪费系统资源,并且使得系统响应速度变慢

在这里插入图片描述

那如何解决这种问题呢?那就是使用LongAdder,至于LongAdder是如何解决这种问题的,就不在这里继续展开描述了,我们留到下一篇文章继续讲解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/767242.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

百度智能汽车负责人储瑞松离职,智驾重心转向ANP3

作者 | 王博 HiEV从多个信息源获悉&#xff0c;百度集团副总裁、百度智能汽车事业部总经理储瑞松将从百度离职。一位知情人士透露&#xff0c;储瑞松「即将启程&#xff0c;返回美国」。 继百度Apollo技术骨干郭阳离职后&#xff0c;储瑞松的变动&#xff0c;更加直白地反映出百…

电动汽车高压测试方案

针对电动汽车道路试验的要求&#xff0c;风丘科技携手德国IPETRONIK共同推出了电动汽车高压测试方案。电动汽车测试通常有两种测量手段&#xff1a;第一种是测量模拟量信号&#xff0c;包括电压、电流、温度和高压&#xff1b;第二种是使用数据记录仪或CAN卡从车辆总线读取数据…

你一定要收好这个系统性能测试用例模板

引言 文档目的 [简述本文档的目的] 适用范围 [指明本文档的适用范围和读者对象。如本测试计划是在策略和方法的高度说明如何计划、组织和管理测试项目。测试计划应包含足够的信息&#xff0c;使测试人员明白项目需要做什么、是如何运作的。另外&#xff0c;测试计划只是测试的…

刚体运动学-速度和加速度的表示方法(连体坐标系和世界坐标系)

0. 符号定义 自己画了一个图 下标 b b b是连体坐标系原点 O b O_b Ob​相对世界坐标系原点 O p O_p Op​的矢量在世界坐标系下的表示。下标 p p p是观察点相对世界坐标系原点 O p O_p Op​的矢量在世界坐标系下的表示。下标 p / b p/b p/b是观察点相对连体坐标系原点 O b O_b…

Python实现将pdf,docx,xls,doc,wps,zip,xlsx,ofd链接下载并将文件保存到本地

前言 本文是该专栏的第31篇,后面会持续分享python的各种干货知识,值得关注。 在工作上,尤其是在处理爬虫项目中,会遇到这样的需求。访问某个网页或者在采集某个页面的时候,正文部分含有docx,或pdf,或xls,或doc,或wps,或ofd,或xlsx,或zip等链接。需要你使用python自…

Yalmip入门教程(2)-变量定义和操作

博客中所有内容均来源于自己学习过程中积累的经验以及对yalmip官方文档的翻译&#xff1a;https://yalmip.github.io/tutorials/ 1.决策变量的定义 1.1 sdpvar 上文简单介绍了sdpvar函数的用法&#xff0c;接下来将对其进行详细介绍。复习一下&#xff0c;sdpvar函数的基本语…

window10安装telnet

1、打开控制面板 2、点击程序和功能 3、点击启用或关闭Windows功能 4、选中Telnet客户端&#xff0c;然后点击确定&#xff0c;然后就可以使用telnent 主机 端口来查看本地是否能连通该主机的该端口。

Linux系统部署Tomcat详细教程(图文讲解)

前言&#xff1a;本篇博客教大家如何一步一步使用Linux系统去部署自己的Tomcat服务器&#xff0c;每一行代码都是我自己严格执行过的&#xff0c;共分为了8点进行阐述&#xff0c;逻辑清晰&#xff01; 目录 一、安装JDK环境 二、准备Tomcat安装包 三、安装Tomcat 四、配置…

【前端】求职必备知识点1-HTML

文章目录 DOCUTYPE的作用HTML语义化HTML5新特性iframedefer 和 async 标题链接【前端】求职必备知识点1-HTMLhttps://blog.csdn.net/karshey/article/details/131795380 DOCUTYPE的作用 DOCTYPE是document type (文档类型) 的缩写。 是HTML5中一种标准通用标记语言的文档类型…

《TCP IP网络编程》第七章

第七章&#xff1a;优雅的断开套接字的连接 TCP 的断开连接过程比建立连接更重要&#xff0c;因为连接过程中一般不会出现大问题&#xff0c;但是断开过程可能发生预想不到的情况。因此应该准确掌控。所以要掌握半关闭&#xff08;Half-close&#xff09;&#xff0c;才能明确断…

131、仿真-基于51单片机智能电子称HX711报警仿真设计(程序+原理图+PCB图+Proteus仿真+参考论文+元器件清单等)

摘 要 电子秤是将检测与转换技术、计算机技术、信息处理、数字技术等技术综合一体的现代新型称重仪器。它与我们日常生活紧密结合息息相关。 电子称主要以单片机作为中心控制单元&#xff0c;通过称重传感器进行模数转换单元&#xff0c;在配以键盘、显示电路及强大软件来组成…

【手撕C语言基础】递归

(꒪ꇴ꒪ ),hello我是祐言博客主页&#xff1a;C语言基础,Linux基础,软件配置领域博主&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff01;送给读者的一句鸡汤&#x1f914;&#xff1a;集中起来的意志可以击穿顽石!作者水平很有限&#xff0c;如果发现错误&#x…

数据结构(王道)——数据结构之 二叉树

一、数据结构之 二叉树概念&#xff1a; 特殊的二叉树结构&#xff1a; 满二叉树完全二叉树 二叉排序树 平衡二叉树 二叉树基本概念总结&#xff1a; 二、二叉树的常用性质&#xff1a; 1、叶子结点比二分支结点多一个

汽车的空气悬架的功能以及发展趋势

空气悬架能实现什么功能以及发展趋势 了解空气悬架之前,首先得快速了解什么是悬架。 教科书说法是: 悬架系统是汽车的车架与车桥或车轮之间的一切传力连接装置的总称。悬架系统基本构成有弹性元件(各类弹簧,缓冲作用);减震元件(减震器,减震作用);导向机构(控制臂等…

技术分享:如何用pytest_addoption切换自动化测试环境?

前言 在目前互联网公司中&#xff0c;都会存在多个测试环境&#xff0c;那么当我们编写的自动化想要在多套测试环境下进行运行时&#xff0c;如何使用&#xff1f; 大多数人想到的可能是通过将我们自动化代码中的地址修改成不同环境。 但是这时候就会增加一些工作量&#xf…

智安网络|移动安全的转型:零信任如何重新定义格局

数字化转型和远程/移动办公的常态化已经成为许多企业的现实。这一转变为企业带来了许多便利&#xff0c;但同时也引入了前所未有的风险&#xff0c;涉及员工的隐私、个人身份和特权访问凭证。尤其是在经济衰退和疫情的持续影响下&#xff0c;许多企业不得不在提高生产力的同时面…

260道网络安全工程师面试题汇总(附答题解析+配套资料)

由于我之前写了不少网络安全技术相关的文章和回答&#xff0c;不少读者朋友知道我是从事网络安全相关的工作&#xff0c;于是经常有人私信问我&#xff1a; 我刚入门网络安全&#xff0c;该怎么学&#xff1f; 想找网络安全工作&#xff0c;应该要怎么进行技术面试准备&…

Linux gdb汇编调试

文章目录 一、示例代码二、gdb汇编指令2.1 step/stepi2.2 next/nexti2.3 info registers2.4 set2.5 x2.6 rip寄存器2.7 rip 寄存器 参考资料 一、示例代码 &#xff08;1&#xff09; #include <stdio.h>int add(int a, int b) {return a b; }int main() {int a 3;in…

Linux安装或者升级cmake,例子为v3.10.2升级到v3.25.0(自己指定版本)

1. 引言 系统: ubuntu 1804 64位点我进入清华源-GCC链接: https://cmake.org/files/v3.25/ 先卸载自己电脑上的cmake。 cmake -version sudo apt-get autoremove cmake2. 下载编译指定版本cmake 可以根据自己的需求下载对应版本的cmake。 cd ~ && mkdir cmake &…

3Ds max图文教程:高精度篮球3D建模

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 第 1 步。使用以下设置在顶部视口上创建球体&#xff1a; 第 2 步。将球体转换为可编辑的多边形&#xff1a; 第 3 步。转到 Edge 子对象级别并剪切以下边缘&#xff1a; 第 4 步。选择以下边&#xff0c;然…