衆所周知 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彙編指令的語義。