JMM在X86下的原理与实现

Java的happen-before模型

  • 众所周知 Java有一个happen-before模型,可以帮助程序员隔离各个平台多线程并发的复杂性,只要Java程序员遵守happen-before模型就不用担心多线程内存排序或者缓存可见性的问题

  • 摘自周志明老师的JMM章节

程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行 发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循 环等结构。

管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这 里必须强调的是“同一个锁”,而“后面”是指时间上的先后。

volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量 的读操作,这里的“后面”同样是指时间上的先后。

线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检 测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止 执行。

线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程 的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。

对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。

传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出 操作A先行发生于操作C的结论。

一个来自技术交流群里的提问

  • 问题

https://club.perfma.com/question/2079981

笔者根据问题扩展的3个demo

  • DEMO1 死循环
public class ThreadNumberDemo {

    static int num = 0;

    public static void main(String[] args) {
new Thread(()->{
System.out.println("Child:" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println("Child End:" + num);
}).start(); System.out.println("Main:" + num);
while(num == 0){
}
System.out.println("Main exit");
}
}
  • DEMO2 退出循环
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadNumberDemo2 {

    static int num = 0;

    static AtomicInteger flushCache = new AtomicInteger(0);

    public static void main(String[] args) {
new Thread(()->{
System.out.println("Child:" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println("Child End:" + num);
}).start();
System.out.println("Main:" + num);
while(num == 0){
flushCache.getAndAdd(1) ;
}
System.out.println("Main exit");
}
}
  • DEMO3 退出循环
public class ThreadNumberDemo3 {

    static int num = 0;

    volatile static int flushCache = 0;

    public static void main(String[] args) {
new Thread(()->{
System.out.println("Child:" + num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println("Child End:" + num);
}).start(); System.out.println("Main:" + num);
while(num == 0){
flushCache ++;
}
System.out.println("Main exit");
}
}
  • 笔者为什么不按常理出牌直接在DEMO1的基础上给num加上volatile?

  • 如果在num变量上加上volatile 则满足了 周志明老师所介绍的 HappenBefore 规则3,而对原子变量跟volatile变量flushCache的操作并不满足任何所谓的happen-before情况,因为在DEMO2 DEMO3整个程序只有主线程访问了flushCache这个变量

volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量 的读操作,这里的“后面”同样是指时间上的先后。

Volatile与原子变量的原理

反编译与调试

  • 笔者凭着好奇心决定尝试反编译看看源码

笔者的Linux跟JDK环境

Distributor ID:	Ubuntu
Description: Ubuntu 20.04.1 LTS
Release: 20.04
Codename: focal openjdk 11.0.9.1 2020-11-04
OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)
  • 读者如果想亲自动手实验 请按照下面两个教程 将hsdis-amd64.so放到对应的JDK目录下

https://juejin.cn/post/6844903656806940686

https://github.com/liuzhengyang/hsdis

  • 请读者注意,每次启动的Java进程内存地址都会变化,下面所有的地址都是笔者调试时的地址,

    读者要根据自己生成的信息 自行更改汇编代码的地址

  • 步骤1 编译java文件

javac ThreadNumberDemo.java

得到ThreadNumberDemo.class文件

  • 步骤2 执行如下命令
java ThreadNumberDemo
  • 步骤3 观察

此时程序并未退出,如下图中 占用笔者大量CPU资源

  • 步骤4 使用反编译插件 + GDB调试

    退出刚才的Java进程,执行如下命令
java -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation
-XX:LogFile=/tmp/log -XX:+PrintAssembly -XX:PrintAssemblyOptions=intel
-XX:-BackgroundCompilation -XX:+UnlockDiagnosticVMOptions ThreadNumberDemo
  • 步骤5 观察/tmp/log文件 查看反编译生成的文件信息 当看到main函数相关的汇编代码以及注释生成后 执行如下命令
sudo gdb -p {pid}

上面的{pid} 请读者以自己机器上运行的Java进程pid为准,笔者这里前面展示的图片中有两个Java进程的pid,读者可以分别用gdb attach上去尝试调试

  • 步骤6 附加后 查看/tmp/log 以及Java源文件

附加java进程后直接跳过

GDB 执行

set disassembly-flavor intel
  • 步骤7 反编译对比 /tmp/log

GDB 执行

disass 0x00007f27a0371b7f,0x00007f27a0371b89

读者需要自行根据 /tmp/log文件中的信息(通过在/tmp/log 搜索Java源文件文件中对应的行号 即可看到对应的汇编代码), 决定disass 后面两个地址,注意中间有一个 ,符号

  • 步骤8 设置breakpoint 跟进代码
break *0x00007f27a0371b7f
break *0x00007f27a0371b86
break *0x00007f27a0371b89

接下来使用c调试 发现死循环如下图

通过GDB 可以看到r10寄存器内的指针指向的内存地址存储的变量为0 eax寄存器中存储的值同样为0

笔者根据上图显示的结果猜测主线程并没有观测到main函数创建的子线程对num的写操作,从r10指针 0x7f27b7848000 (num变量的地址) 来打印num,

几次循环下来均为0,num == 0 这个条件一直成立是导致主线程不断循环的原因占用CPU的原因。

  • 步骤9 笔者通过GDB 如下设置PC指针跳出循环 验证程序正常退出 如下图
set var $pc=0x00007f27a0371b8b

  • DEMO1小结 死循环的根本原因在于主线程无法观测到子线程对num的更新的值,据笔者推测是多线程缓存可见性的问题

  • DEMO2 如下图

  • DEMO3 如下图

总结

DEMO2 DEMO3 反汇编后均找到lock指令,基本上可以判断JVM在X64机器上对原子变量跟volatile的实现都使用了X86汇编语言lock指令的语义,

根据笔者在Stack Overflow上的一些资料浏览得出结论--lock语义具有内存栅栏的功能,能解决DEMO1(num变量)内存不可见的问题,

另外DEMO2 DEMO3均未使用Happen-Before模型,仅使用了X86的lock汇编指令的语义。

一点补充

JMM在X86下的原理与实现的更多相关文章

  1. MinHook测试与分析(x86下 E8,E9,EB,CALL指令测试,且逆推测试微软热补丁)

    依稀记得第一次接触Hook的概念是在周伟民先生的书中-><<多任务下的数据结构与算法>>,当时觉得Hook的本质就是拦截,就算到现在也是如此认为. 本篇文章是在x86下测 ...

  2. 【原创】X86下ipipe接管中断/异常

    目录 X86 ipipe接管中断/异常 一.回顾 二.X86 linux异常中断处理 1. 中断门及IDT 2. 初始化门描述符 2.1 早期异常处理 2.2 start_kernel中的异常向量初始 ...

  3. 在CentOS6.9 x86下编译libusb-1.0.22遇到的两个问题

    OS版本:CentOS 6.9 x86,内核版本2.6.32 问题一:configure.ac:36: error: Autoconf version 2.69 or higher is requir ...

  4. Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题

    Android 设备的CPU类型(通常称为”ABIs”) 引用: https://blog.csdn.net/ouyang_peng/article/details/51168072 armeabiv ...

  5. 【转】Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题

    转载地址:http://blog.csdn.net/ouyang_peng/article/details/51168072 Android 设备的CPU类型(通常称为”ABIs”) x86: 平板. ...

  6. 我的Android进阶之旅------&gt;Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题

    Android 设备的CPU类型通常称为ABIs 问题描写叙述 解决方法 1解决之前的截图 2解决后的截图 3解决方法 4建议 为什么你须要重点关注so文件 App中可能出错的地方 其它地方也可能出错 ...

  7. Windows x86 下的 静态代码混淆

    0x00  前言 静态反汇编之王,毫无疑问就是Ida pro,大大降低了反汇编工作的门槛,尤其是出色的“F5插件”Hex-Rays可以将汇编代码还原成类似于C语言的伪代码,大大提高了可读性.但个人觉得 ...

  8. 【转载】Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题

    转自:[欧阳鹏]http://blog.csdn.net/ouyang_peng Android 设备的CPU类型(通常称为”ABIs”) armeabiv-v7a: 第7代及以上的 ARM 处理器. ...

  9. 我的Android进阶之旅------>Android 关于arm64-v8a、armeabi-v7a、armeabi、x86下的so文件兼容问题

    Android 设备的CPU类型通常称为ABIs 问题描述 解决方法 1解决之前的截图 2解决后的截图 3解决方法 4建议 为什么你需要重点关注so文件 App中可能出错的地方 其他地方也可能出错 使 ...

随机推荐

  1. ADF 第五篇:转换数据

    映射数据流(Mapping Data Flow)的核心功能是转换数据,数据流的结构分为Source.转换和Sink(也就是Destination),这种结构非常类似于SSIS的数据流. 在数据流中,数 ...

  2. 【PY从0到1】第二节 字符串和数字

    #本课程都以代码形式呈现.现在进入Python的基础内容的学习. #由于本课程是从0开始分享,所以Python的基础内容是必不可少的.这也是Python量化的必经之路. #下面进入正题. #一般用'# ...

  3. Spark性能优化指南

    1 Spark开发调优篇 原则一:避免创建重复的RDD 原则二:尽可能复用同一个RDD 原则三:对多次使用的RDD进行持久化 原则四:尽量避免使用shuffle类算子 因此在我们的开发过程中,能避免则 ...

  4. python的常量与变量

    1.Python属于强类型编程语言,Python解释器会根据赋值或运算来自动判断变量的类型.Python还是一种动态类型语言,变量的类型也是可以随时变化的. >>> x=3 > ...

  5. 由两个问题引发的对GaussDB(DWS)负载均衡的思考

    摘要:GaussDB(DWS)的负载均衡通过LVS+keepAlived实现.对于这种方式,需要思考的问题是,CN的返回结果是否会经过LVS,然后再返回给前端应用?如果经过LVS,那么,LVS会不会成 ...

  6. JavaDailyReports10_15

    2020-10-15 16:12:16 今天学习了如何实现倒计时控制程序的运行: 1 package timer; 2 3 import java.util.Calendar; 4 import ja ...

  7. webpack相关的问题

    随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,如react的jsx代码必须编译后才能在浏览器中使用:又如sass和less的代码浏览器也是不支持的. 而如果摒弃了这些开发框 ...

  8. [每日一题]面试官问:Async/Await 如何通过同步的方式实现异步?

    关注「松宝写代码」,精选好文,每日一题 ​时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  9. spark使用idea向yarn提交报错:Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/jersey/api/client/config/ClientConfig

    解决方法: 找到1.19版本放到spark的jars目录下

  10. 杭电OJ----1097:一个难题(c++)

    问题描述 lcy给feng5166,lwg,JGShining和Ignatius带来了一个难题:给了a和b,如何知道a ^ b.每个人都反对这个BT问题,所以lcy使问题比开始容易. 这个难题描述了: ...