Java精通并发-CAS底层实现与AtomicInteger源码剖析、关于CAS问题描述
CAS底层实现:
在上一次https://www.cnblogs.com/webor2006/protected/p/12874390.html对于CAS进行了了解,先回忆一下关键要点:
那学了它有啥用呢?当然是有用的,一个概念的提出总是有实践者对其进行应用的,该实践者就是Java并发包,它里面提供大量的原子操作的基础组件,比如:AtomicInteger、AtomicLong...,其实这些原子组件都是位于并发包中的这个包中:
这次对CAS再进一步深挖它,首先先来看一下它相关的一个理论:
对于CAS来说,其操作数主要是涉及到如下三个:
1、需要被操作的内存值V:也就是最终对其要进行修改的内存的值,也就是最终我们要完成的一个目标。
2、需要进行比较的值A;
3、需要进行写入的值A:也就是在正式写入之前,需要对值进行一次比较。
而2、3步骤的比较更新规则是按这种来进行的:只有当V==A的时候,CAS才会通过原子操作的手段将V的值更新为B。
注意:这里涉及到比较和交换两个动作,但是在CAS的语义下这俩操作其实是一个原子操作,底层是用一个指令来达成的,而既然有比较那就存在比较成功和失败两种可能,而如果比较成功的话那就直接交换更新了,而如果比较失败的话则就会不断循环的进行交换尝试的过程,这个在上次已经有所说明。
下面咱们以使用AtomicInteger为例,进一步理解CAS的本质,代码比较简单,直接贴出来:
package com.javacurrency.test6; import java.util.concurrent.atomic.AtomicInteger; public class MyTest2 { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5); System.out.println(atomicInteger.get()); //将老的值5改成8,同时将老的值作为结果返回来 System.out.println(atomicInteger.getAndSet(8)); System.out.println(atomicInteger.get()); //先取出AtomicInteger的值,然后再将其自增1,返回的还是老的值 System.out.println(atomicInteger.getAndIncrement()); System.out.println(atomicInteger.get());//获取最新值 } }
运行看一下:
5
5
8
8
9
Process finished with exit code 0
下面对结果进行一下解释,其实根据代码的注释也能理解输出结果为啥是上面这些,来,还是过一遍:
AtomicInteger源码剖析:
了解成员变量:
对于上面这个代码比较容易理解,也比较没啥意义,有意义当然就是通过分析它里面的源码来对咱们上面的理论进行一下巩固,所以源码走起:
下面先来对这三个成员变量有一个基本了解:
Unsafe这个是JDK中的只能被内部使用的类,不能由我们普通开发者来调用的,看下它的包名就晓得了:
可以看到它并非是开源的, 它是实现原子操作的一个主要的操作对象。接下来再来了解第二个成员变量:
不过。。在了解它之前,先来了解一下它:
它比较好理解,就是它内部所包装的整数值,其中被volatile所修饰了,关于volatile的作用可以参考:https://www.cnblogs.com/webor2006/protected/p/12595201.html,主要是起到变量可见性的作用,当当前的线程对这个变量的值进行修改时,该值就会立马对其它的线程所看到,好,接下来回头再来理解一下这个变量:
它其实是值在对象当中的一个内存偏移量,那。。为啥要搞个这个变量呢?因为最终原子操作需要借助于Unsafe来实现,而它本质上是JNI通过调用C++的代码来实现的,而C++的代码是需要进行内存操作的,所以就定义了这么个偏移量,而看一下它的初始化,其实就是用Unsafe来获取的:
而瞅一下objectFieldOffset是native方法么?
嗯,确实是。
具体相关API底层揭秘:
ok,在对它里面的成员变量的含义有了一个基本认识之后,接下来咱们则从代码的调用API角度来分析其底层实现的机理,对于咱们的代码用到了几个相关的API,回忆一下:
咱们先来分析它:
源码跟进去:
其中它又调用了Unsafe类中的getAndSetInt(),从这点又能感受到其实实现原子的核心就是靠Unsafe,而传了三个参数,先记住一下它们的含义:
跟进去看一下:
由于它不是开源的所以此时看到的是IDE所反编译的代码,而如果想要查看真正的源码则可以通过OpenJDK来查看,不过这里暂时不需要看得这么细,从IDE反编译的代码上就可以看到它是在一个循环当中来执行的,这也是在JDK中很多的库利用CAS操作的一个基本的使用模式,首先先来获取到值,然后不断的对值进行比较和交换尝试,而其中比较交换它是一个NDK方法,如下:
而要想更好的理解其交换原理就得对这四个参数进行一个了解,下面来一一对其解释一下:
- var1:代表待操作对象的内存地址,所以我们传的是当前对象:
- var2:代表了要操作的值在对象中的偏移量:
- var5:变量的预期值,也就是要比较的值:
也就是上面CAS理论中的三个操作数中的这个: - var4:表示既将要更新的值,
另外有一点需要知道:
而且可以看下对于其它类型也基本都是同样的写法:
在之前咱们也说过UnSafe是一个不对普通开发者开放的类,怎么来体现呢,其实可以从它的定义中得知:
而且在获取实例时也有个判断只有系统类加载器才能够获得,如果不是则直接抛异常了:
根据之前学习JVM中的类加载器可知,对于我们自己编写的代码是由应用类加载器来加载的,很显然此方法不能直接被我们调用,但是!!!可以用反射获取,但是由于Unsafe它是直接操纵C++底层的,所以最好别自己获取实例来做一些操作,
关于CAS问题描述:
- 循环开销问题:并发最大的情况下会导致线程一直自旋,因为对于原子操作的实现是在一个do..while当中嘛:
- 只能保证一个变量的原子操作:很明显对于AtomicInterger就是对一个整形变量是一个原子操作,而如果想要实现多个整型变量的原子操作则用它就不行了,不过并发包中有一个能满足这种需求的类存在:AtomicReference
-
ABA问题:啥是ABA问题呢? 比如说某一个变量,它的初始值是1,然后打算对它进行一个CAS更新操作;而在CAS更新操作未真正将该变量的值进行写入之前,恰好有另一个线程来了,此时这个线程有可能将这个变量由1变为3,而紧接着又将变量的值由3改回到了1;好,此时又回到第一个线程,它看到的结果肯定是1,而进行CAS操作时会进行比较发现变量的值木有发现改变(但是这是一个假象,实际是已经被另一个线程更改过了只是又将值改成还原的状态了而已),因此该CAS操作依然是能够成功的,而如果从程序的正确性来讲其实这没啥问题,最终结果不会有啥影响,但是!!如果从程序的语义角度则就不正确了,因为毕境变量在进行CAS操作时被其它线程是改过的,很明显CAS就无法解决这样的问题了,但是其实要解决这个问题其实有两种思路:一种是增加一个版本号,如果有更改则对版本号进行增加,或者是时间戳也可以,最终在CAS比较时除了比较值之外,可以将版本号或时间戳也进行比较就可以避免这种ABA问题了。