欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

白云观--多线程系列之CAS和原子操作(十)

程序员文章站 2022-07-06 15:27:16
本章节小宋带大家学习一下CAS的相关知识和与之密切相关的原子操作。// A code blockvar foo = 'bar';目录CAS概念比较并交换的过程Java实现CAS的原理 - Unsafe类原子操作 - AtomicInteger类源码简析解决CAS原子操作的三大问题ABA问题循环时间长开销大只能保证一个共享变量的原子操作CAS概念CAS的全称是:Compare And Swap(比较并交换)。CAS有三个值:V: 要更新的变量(var)E: 预期值(expected)....

本章节小宋带大家学习一下CAS的相关知识和与之密切相关的原子操作。

CAS概念

CAS的全称是:Compare And Swap(比较并交换)。

CAS有三个值:

  1. V: 要更新的变量(var)
  2. E: 预期值(expected)
  3. N: 新值(new)

比较并交换的过程

判断V是否等于E,如果等于将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

所以这里的预期值E本质上指的是旧值

举个简单的例子:

  1. 有一个多线程去共享变量s(原本等于5),现在线程A想把它设置为新的值6。
  2. CAS怎么去做这件事情呢,首先我们用s去和5做对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,s的值被设置成了6;
  3. 如果不等于5,说明s被其它线程改过了(比如现在s的值为2),那么就什么也不做,此次CAS失败,i的值仍然为2。

在这个例子中,s就是V,5就是E,6就是N。

那有没有可能我在判断了i为5之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?

不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性

当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

Java实现CAS的原理 - Unsafe类

前面提到,CAS是一种原子操作。那么Java是怎样来实现CAS的呢?我们知道,在Java中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使用c或者c++去实现。

在Java中,有一个Unsafe类,在sun.misc包中。它里面包括了一些native方法,其中就有几个关于CAS的。

白云观--多线程系列之CAS和原子操作(十)
他们都是public final native的。

Unsafe中对CAS的实现就是C++写的,它的具体实现和操作系统,CPU都有关系

Linux的X86下主要是通过 cmpxchgl 这个指令在CPU级完成CAS操作的,但在多处理器情况下必须使用lock指令加锁来完成。当然不同的操作系统和处理器的实现会有所不同,大家可以自行了解。

当然,Unsafe类里面还有其它方法用于不同的用途。比如支持线程挂起和恢复的parkunpark, LockSupport类底层就是调用了这两个方法。还有支持反射操作的allocateInstance()方法。
白云观--多线程系列之CAS和原子操作(十)

原子操作 - AtomicInteger类源码简析

上面介绍了Unsafe类的几个支持CAS的方法。那Java具体是如何使用这几个方法来实现原子操作的呢?

JDK提供了一些用于原子操作的类,在java.util.concurrent.atomic包下面。在JDK 11中,有如下17个类:
白云观--多线程系列之CAS和原子操作(十)
从名字就可以看得出来这些类大概的用途:

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新字段(属性)

这里我们以AtomicInteger类的getAndAdd(int delta)方法为例,来看看Java是如何实现原子操作的。

直接进行源码解析:
白云观--多线程系列之CAS和原子操作(十)
这里的unsafe就是一个Unsafe对象
白云观--多线程系列之CAS和原子操作(十)
所以其实AtomicInteger类的getAndAdd(int delta)方法就是调用Unsafe类的getAndAddInt方法来实现的:
白云观--多线程系列之CAS和原子操作(十)
这个方法是在JDK 1.8以后才新增的。在JDK1.8之前,AtomicInteger源码实现有所不同,是基于for死循环的,有兴趣的话可以自己了解一下。

我们来一步步解析这段源码。首先,对象var1是this,也就是一个AtomicInteger对象。然后var2是一个常量valueOffset。这个常量是在AtomicInteger类中声明的:
白云观--多线程系列之CAS和原子操作(十)
同样是调用的Unsafe的方法。从方法名字上来看,是得到了一个对象字段偏移量。

用于获取某个字段相对Java对象的“起始地址”的偏移量。

一个java对象可以看成是一段内存,各个字段都得按照一定的顺序放在这段内存里,同时考虑到对齐要求,
可能这些字段不是连续放置的,

用这个方法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量,因为是相对偏移量,
所以它其实跟某个具体对象又没什么太大关系,跟class的定义和虚拟机的内存模型的实现细节更相关。

继续看源码。前面我们讲到,CAS是“无锁”的基础,它允许更新失败。所以经常会与while循环搭配,在失败后不断去重试(自旋)。
白云观--多线程系列之CAS和原子操作(十)
这里声明了一个var5,也就是要返回的值。从getAndAddInt来看,它返回的应该是原来的值,而新的值是var5 + var4。

这里使用的是do-while循环。这种循环不多见,它的目的是保证循环体内的语句至少会被执行一遍。这样才能保证return 的值var5是我们期望的值。

循环体的条件是一个CAS方法:
白云观--多线程系列之CAS和原子操作(十)
可以看到,最终其实是调用的我们之前说到的Unsafe类的CAS native方法。

而在JDK 9开始,加了@HotSpotIntrinsicCandidate注解。
这个注解允许HotSpot VM自己来写汇编或IR编译器来实现该方法以提供性能。
HotSpot VM也有可能手动来实现Java中CAS方法真正含义的功能。

它跟volitile有关。

简单来说,上面的CAS操作仅保留了volatile自身变量的特性,而出去了happens-before规则带来的内存语义。也就是说,它无法保证处理操作目标的volatile变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性。这在一定程度上可以提高性能。

再回到循环条件上来,可以看到它是在不断尝试去用CAS更新。如果更新失败,就继续重试。那为什么要把获取“旧值”v的操作放到循环体内呢?其实这也很好理解。前面我们说了,CAS如果旧值V不等于预期值E,它就会更新失败。说明旧的值发生了变化。那我们当然需要返回的是被其他线程改变之后的旧值了,因此放在了do循环体内。

解决CAS原子操作的三大问题

继续讲解CAS进行原子操作的三大问题及其解决方案。

ABA问题

所谓ABA问题,就是一个值原来是A,变成了B,又变回了A。这个时候使用CAS是检查不出变化的,但实际上却被更新了两次。

ABA问题的解决思路是在变量前面追加上版本号或者时间戳。从JDK 1.5开始,JDK的atomic包里提供了一个类AtomicStampedReference类来解决ABA问题。

这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用CAS设置为新的值和标志。
白云观--多线程系列之CAS和原子操作(十)

循环时间长开销大

CAS多与自旋结合。如果自旋CAS长时间不成功,会占用大量的CPU资源。

解决思路是让JVM支持处理器提供的pause指令。

pause指令能让自旋失败时cpu睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的CPU流水线重排的代价也会小很多。

只能保证一个共享变量的原子操作

两种解决方案:

  1. 使用JDK 1.5开始就提供的AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作;
  2. 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

讲到这里本章对多线程系列的CAS和原子操作知识讲解也就结束了,如果想了解更多知识可以在对应的专栏中看系列文章,谢谢大家的观看,希望能给各位同学带来帮助。如果觉得博主写的还可以的,可以点赞收藏。 ????

本文地址:https://blog.csdn.net/weixin_42236165/article/details/112556037