• Java并发编程之CAS二源码追根溯源


    Java并发编程之CAS二源码追根溯源

    在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理。

    本篇是《凯哥(凯哥Java:kagejava)并发编程学习》系列之《CAS系列》教程的第二篇:从源码追根溯源查看CAS最底层是怎么实现的。

    本文主要内容:CAS追根溯源,彻底找到CAS的根在哪里。

    一:查看AtomicInteger.compareAndSet源码

    通过上一篇文章学习,我们知道了AtomicInteger.compareAndSet方法不加锁可以保证原子性(其原理就是unsafe+cas实现的),我们来看看其源码:

    思考1:变量可见性

    AtomicInteger对象(下文凯哥简称:atoInteger)怎么保证变量内存可见性呢?

    查看源码:

    思考2:为什么上一篇13行的i.compareAndSet(1,1024)是false

    我们来看看atoInteger的compareAndSet方法。凯哥在上面添加了注释。

    在调用unsafe的compareAndSwapInt这个方法的时候,unsafe是什么?this指的是什么?valueOffset又是什么呢?

    我们接着查看atoInteger源码:

    我们发现Unsafe以及valueOffset都是从一个对象中获取到的。

    那么this指的是什么?其实this就是当前atoInteger对象。

    那么Unsafe对象在哪里呢?

    我们想要看源码,怎么查看呢?发现不能看源码啊。别急,这个文件的源码可以从openJdk的源码中查到。

    接着,我们来查看OpenJdk8的源码:

    (PS:下载OpenJdk8源码凯哥这里就不赘述了。在文章最后,凯哥给出)

    下载完,解压之后,文件位置:openjdkjdksrcshareclassessunmisc。如下图:

    我们来看看Unsafe类上面的注解:

    A collection of methods for performing low-level, unsafe operations.

    什么意思呢?用于执行底层的(low-level,)、不安全操作的方法的集合。

    就是说,这个类可以直接操作底层数据的。

    需要说明的是:在这个对象中大量的方法使用了native来修饰(据网友统计高达82个)

    我们知道,Java的方法使用native关键字修饰的,说明这个方法不是Java自身的方法(非Java方法),可能调用的是其他语言的。如C或C++语言的方法。

    我们再来看看:unsafe.objectFieldOffse()中的

    这个方法就是返回一个内存中访问偏移量。

    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

    在unsafe类中compareAndSwapInt方法也是native的。我们在来看看这个方法调用操作系统底层C++的代码:

    说明:

    jint *addr:主内存中的变量值

    old:对象工作区域的值

    new_val:将要改变的值。

    这三个是不是很熟悉,对。就是CAS的三个参数。

    分析第13行为什么返回false:

    在11行的时候,设置主内存的变量值V=1.

    在12行后,更新为V=2020了。

    当执行到第13行的时候,

    主内存:V=2020

    程序工作区变量值jint *addr A的值:A=1

    new_val:1024。

    从调用C++代码我们可以分析到:

    在第5行的时候,因为1!=2020,所以return 的result就是false.

    所以第13行输出的是false.

    思考3:atoInteger.getAndIncrement()是怎么保证数据一致性的

    调用的是getAndAddInt方法。接着查看unsafe的源码,就会发现CAS保证原子性的终极代码。

    CAS保证原子性终极方法,如下图:

    看看:getObjectVolatile。方法发现是native.如下图:

    再来看看compareAndSwapObject:

    发现是native修饰的方法。说明不是Java的方法。这个我们等会再细说。

    先来研究getAndSetObject:

    源码:

    我们来模拟:atoInteger.getAndIncrement();

    假设默认值是0. 主内存的值是0

    在调用getAndSetObject方法的几个参数说明:

    Var1:当前atoInteger对象

    Var2:当前偏移量(内存地址所在位置。如:三排四列)

    Vart4:默认就是1

    Var5:获取到的主内存的值

    Var5+var4:将要更新的值。

    从源码,我们看到是do while语句。为什么不是while语句呢?因为先要获取到主内存中变量最新的值,然后再判断。所以选用了do while语句。

    我们来看看当CPU1线程1和CPU2线程B来执行的时候:

    两个线程都从主内存copay了i的值到自己工作内存空间后,进行+1的操作。

    假设线程1再执行+1操作后,准备往主内存回写数据的时候,CPU1被挂起。然后CPU2竞争到资源之后,也操作i+1后,将更新后的值回写到了主内存中。然后切换到CPU1了,CPU1接着执行。对比代码分析:

    线程1在执行do后得到的值var5=1而不是0

    然后while里面执行:var1和var2运算后的结果是0(工作区的值)。

    因为0!=5 .所以this.comparAndSwapInt的值是false.

    又因为前面有个! 非得符号。也就是!false。我们知道!false就是true.

    也就是while(true)。While(true)后,接着循环执行。线程会放弃原有操作,重新从主内存中获取到最新数据(此时就是1了),然后再进行操作后。

    又到了do,获取在主内存最新数据是1.接着走while()

    因为,var1,var2获取到工作区的值是1 var5也等于1.1=1,成立了,执行var5+var5=1+1=2,来更新主内存的数据后返回true.

    又因为前面有个!非的符号。所以就是while(!true),也就是while(false)。退出循环,返回var5的值。

    结论:

    通过上面的运行分析,我们发现atoInteger的getAndIncrement方法保证原子性是unsafe+CAS来保证变量原子性的(其中do while语句就是后面我们将要学到的自旋)

  • 相关阅读:
    LeetCode120 Triangle
    LeetCode119 Pascal's Triangle II
    LeetCode118 Pascal's Triangle
    LeetCode115 Distinct Subsequences
    LeetCode114 Flatten Binary Tree to Linked List
    LeetCode113 Path Sum II
    LeetCode112 Path Sum
    LeetCode111 Minimum Depth of Binary Tree
    Windows下搭建PHP开发环境-WEB服务器
    如何发布可用于azure的镜像文件
  • 原文地址:https://www.cnblogs.com/kaigejava/p/12571218.html
Copyright © 2020-2023  润新知