众所周知 Java有一个happen-before模型,能够帮助程序员隔离各个平台多线程并发的复杂性,只要Java程序员遵照happen-before模型就不用担忧多线程内存排序或者缓存可见性的问题java
摘自周志明老师的JMM章节git
程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操做先行 发生于书写在后面的操做。注意,这里说的是控制流顺序而不是程序代码顺序,由于要考虑分支、循 环等结构。程序员
管程锁定规则(Monitor Lock Rule):一个unlock操做先行发生于后面对同一个锁的lock操做。这 里必须强调的是“同一个锁”,而“后面”是指时间上的前后。github
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操做先行发生于后面对这个变量 的读操做,这里的“后面”一样是指时间上的前后。ubuntu
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个动做。缓存
线程终止规则(Thread Termination Rule):线程中的全部操做都先行发生于对此线程的终止检 测,咱们能够经过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止 执行。sass
线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程 的代码检测到中断事件的发生,能够经过Thread::interrupted()方法检测到是否有中断发生。bash
对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。多线程
传递性(Transitivity):若是操做A先行发生于操做B,操做B先行发生于操做C,那就能够得出 操做A先行发生于操做C的结论。并发
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"); } }
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"); } }
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变量的写操做先行发生于后面对这个变量 的读操做,这里的“后面”一样是指时间上的前后。
笔者的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)
https://juejin.cn/post/6844903656806940686
https://github.com/liuzhengyang/hsdis
请读者注意,每次启动的Java进程内存地址都会变化,下面全部的地址都是笔者调试时的地址,
读者要根据本身生成的信息 自行更改汇编代码的地址
步骤1 编译java文件
javac ThreadNumberDemo.java
获得ThreadNumberDemo.class文件
java ThreadNumberDemo
此时程序并未退出,以下图中 占用笔者大量CPU资源
java -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:LogFile=/tmp/log -XX:+PrintAssembly -XX:PrintAssemblyOptions=intel -XX:-BackgroundCompilation -XX:+UnlockDiagnosticVMOptions ThreadNumberDemo
sudo gdb -p {pid}
上面的{pid} 请读者以本身机器上运行的Java进程pid为准,笔者这里前面展现的图片中有两个Java进程的pid,读者能够分别用gdb attach上去尝试调试
附加java进程后直接跳过
GDB 执行
set disassembly-flavor intel
GDB 执行
disass 0x00007f27a0371b7f,0x00007f27a0371b89
读者须要自行根据 /tmp/log文件中的信息(经过在/tmp/log 搜索Java源文件文件中对应的行号 便可看到对应的汇编代码), 决定disass 后面两个地址,注意中间有一个 ,
符号
break *0x00007f27a0371b7f break *0x00007f27a0371b86 break *0x00007f27a0371b89
接下来使用c调试 发现死循环以下图
经过GDB 能够看到r10寄存器内的指针指向的内存地址存储的变量为0 eax寄存器中存储的值一样为0
笔者根据上图显示的结果猜想主线程并无观测到main函数建立的子线程对num的写操做,从r10指针 0x7f27b7848000
(num变量的地址) 来打印num,
几回循环下来均为0,num == 0 这个条件一直成立是致使主线程不断循环的缘由占用CPU的缘由。
set var $pc=0x00007f27a0371b8b
DEMO1小结 死循环的根本缘由在于主线程没法观测到子线程对num的更新的值,据笔者推测是多线程缓存可见性的问题
DEMO2 以下图
DEMO2 DEMO3 反汇编后均找到lock指令,基本上能够判断JVM在X64机器上对原子变量跟volatile的实现都使用了X86汇编语言lock指令的语义,
根据笔者在Stack Overflow上的一些资料浏览得出结论--lock语义具备内存栅栏的功能,能解决DEMO1(num变量)内存不可见的问题,
另外DEMO2 DEMO3均未使用Happen-Before模型,仅使用了X86的lock汇编指令的语义。