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

news/2024/10/18 18:29:20/

图解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++;}// 打印的必然是100System.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变量加100for (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变量加100for (int i = 0; i < 100; i++) {new Thread(() -> {for (int j = 0; j < 100; j++) {Test.addNum1();}}).start();}Thread.sleep(1000);// 得到的结果一定是10000System.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变量加100for (int i = 0; i < 100; i++) {new Thread(() -> Test.addNum100()).start();}Thread.sleep(1000);// 得到的结果就一定是10000System.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类进行自旋+CASpublic 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类进行自旋+CASpublic 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对象和valueOffsetvar6 = 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;// 获取我们的对象referencepublic V getReference() {return pair.reference;}// 获取当前版本号public int getStamp() {return pair.stamp;}// 通过Unsafe类的objectFieldOffset方法获取pair的内存偏移量pairOffsetprivate 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;returnexpectedReference == 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.ppmy.cn/news/970256.html

相关文章

灵遁者诗歌集《禅在禅中》序言篇:写诗激情已去

导读&#xff1a;我当然不能和ChatGPT比写诗歌&#xff0c;我不再渴望写诗歌了&#xff0c;激情褪去了。但《禅在禅中》我会坚持写完。 本文为灵遁者诗歌集《禅在禅中》序言篇&#xff1a; 我今天做了一个奇怪又真实的梦&#xff0c;大概是这样的&#xff0c;梦见有个小伙要跟…

AI教父Hinton最新采访万字实录:ChatGPT和AI的过去现在与未来

杰弗里辛顿&#xff08;Geoffrey Hinton&#xff09;被公认是人工智能的教父&#xff0c;数十年前他就支持和推动了机器学习&#xff0c;随着像ChatGPT这样的聊天机器人引起广泛关注&#xff0c;CBS的主持人于2023年3月初在多伦多的Vector研究所采访了Hinton。 全长40分钟的采访…

内网与外网有什么区别

内网 内网指的就是在某一指定的区域内由多台计算机互联成的计算机组&#xff0c;比如家庭、单位、学校、公司等&#xff0c;是一个小范围的&#xff0c;它可以在空间几千米内实现互联网文件管理、软件、打印机等共享&#xff0c;局域网是封闭的&#xff0c;它可以是两台电脑组…

自己动手做chatgpt:解析gpt底层模型transformer的输入处理

前面我们完成了一些基本概念&#xff0c;如果你对深度学习的基本原理还不了解&#xff0c;你可以通过这里获得更多信息&#xff0c;由于深度学习的教程汗牛充栋&#xff0c;因此我在这里不会重复&#xff0c;而是集中精力到chatgpt模型原理的分析&#xff0c;实现和实践上。Cha…

ChatGPT最新进度,行业应用进程如何?

ChatGPT是OpenAI基于其GPT-3语言模型开发的对话生成AI系统。根据OpenAI的公告&#xff0c;他们已经推出了一系列ChatGPT的相关产品&#xff0c;包括&#xff1a;Codex&#xff1a;用于编程的AI助手&#xff0c;可以基于自然语言生成代码片段&#xff0c;支持多种编程语言。 GP…

ChatGPT最新综述

概述 本文的研究背景是ChatGPT是由OpenAI创建的一种基于大量数据进行训练的大语言模型&#xff0c;它对自然语言处理领域产生了革命性的影响&#xff0c;并推动了大语言模型的能力边界。ChatGPT在大规模上实现了广泛的公众与生成人工智能的互动&#xff0c;进而引发了开发相似…

一次只要0.003美元,比人类便宜20倍!ChatGPT让数据标注者危矣

作者丨好困 来源丨学术头条 编辑丨新智元 点击进入—>3D视觉工坊学习交流群 【导读】最近&#xff0c;来自苏黎世大学的研究团队发现&#xff0c;ChatGPT在多个NLP标注任务上胜过众包工作者&#xff0c;具有较高一致性&#xff0c;且每次标注成本仅约0.003美元&#xff0c;比…

C知道,CSDN 出来的AI尝试

已经上图&#xff0c;算力不知道怎么样。 C知道 (csdn.net)