在上一篇文章中, 我们知道了什么是 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 源码凯哥这里就不赘述了. 在文章最后, 凯哥给出)
下载完, 解压之后, 文件位置: openjdk\jdk\src\share\classes\sun\misc. 如下图:
我们来看看 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 语句就是后面我们将要学到的自旋)
来源: https://www.cnblogs.com/kaigejava/p/12571218.html