CPU內部結構划分
控制單元
運算單元
存儲單元

計算機多硬件多CPU結構:

CPU緩存一致性原則
JMM同步八種操作介紹:
(1)lock(鎖定):作用於主內存的變量,把一個變量標記為一條線程獨占狀態
(2)unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放后的 變量才可以被其他線程鎖定
(3)read(讀取):作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中, 以便隨后的load動作使用
(4)load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作 內存的變量副本中
(5)use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎
(6)assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量
(7)store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作
(8)write(寫入):作用於工作內存的變量,它把store操作從工作內存中的一個變量的值傳送 到主內存的變量中
如果要把一個變量從主內存中復制到工作內存中,就需要按順序地執行read和load操作, 如果把變量從工作內存中同步到主內存中,
就需要按順序地執行store和write操作。但Java內 存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。

JMM三大特性
原子性
匯編指令 --原子比較和交換在底層的支持 cmp-chxg
解決辦法: Synchronized Lock鎖機制 保證任意時刻只有一個線程訪問該代碼塊。
public class VolatileAtomicSample { private static volatile int counter = 0; // volatile無法保證原子性 public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(()->{ for (int j = 0; j < 1000; j++) { counter++; //不是一個原子操作,第一輪循環結果是沒有刷入主存,這一輪循環已經無效 //1 load counter 到工作內存 //2 add counter 執行自加 } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } }
啟動10個線程,每個線程執行自增步驟,count++ 是非原子性的。volatile保證數據的可見性,同時存在CPU緩存鎖機制以及MESI緩存分布式協議,最后打印的值 <= 10000.
CPU為了提升性能,會存在指令編排機制。也就會出現內存屏障 見有序性詳解。
可見性 volatile -- LOCK緩存行(有且僅有一個線程會占有緩存行) + CPU緩存一致性原則MESI(獨占E-->共享S-->修改M--->其他失效I)
public class VolatileVisibilitySample { private boolean initFlag = false; public void refresh(){ this.initFlag = true; //普通寫操作,(volatile寫) String threadname = Thread.currentThread().getName(); System.out.println("線程:"+threadname+":修改共享變量initFlag"); } public void load(){ String threadname = Thread.currentThread().getName(); int i = 0; while (!initFlag){ i++; } System.out.println("線程:"+threadname+"當前線程嗅探到initFlag的狀態的改變"+i); } public static void main(String[] args){ VolatileVisibilitySample sample = new VolatileVisibilitySample(); Thread threadA = new Thread(()->{ sample.refresh(); },"threadA"); Thread threadB = new Thread(()->{ sample.load(); },"threadB"); threadB.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } }
分析如下: 只會打印 "線程:threadA:修改共享變量initFlag".
修改:
因為在線程A里面增加了鎖機制,同時CPU自身也存在時間片切片,導致線程上下文切換,initFlag會從內存中讀取線程B更新的值。
會把線程B嗅探機制打印出來。打印如下:
線程:threadA:修改共享變量initFlag
線程:threadB當前線程嗅探到initFlag的狀態的改變25747425
修改2:使用volatile關鍵字 ====> JMM緩存一致性原則(MESI) + LOCK緩存行
有序性
-- 指令重排 ---> 內存屏障(volatile禁止重排優化 )
查看匯編指令:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高並行度),(單線程)。 即在單線程情況下,不能改變程序運行的結果
程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
double p = 3.14; //1 double r = 1.0; //2 double area = p * r * r; //3計算面積
上面例子中1,2存在指令重排操作,但是1,2不能和第三步存在指令重排操作,否則將改變程序運行的結果。
happen-before原則
1、 程序順序原則,即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行
2. 鎖規則 解鎖(unlock)操作必然發生在后續的同一個鎖的加鎖(lock)之前,也就是說,如果對於一個鎖解鎖后,再加鎖,那么加鎖的動作必須在解鎖動作之后(同一個鎖)
3. volatile規則 volatile變量的寫,先發生於讀,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,
而當該變量發生變化時,又會強迫將最新的值刷新到主內存,任何時刻,不同的線程總是能夠看到該變量的最新值。
4. 線程啟動原則 線程的start()方法先於他的每一個動作,即如果線程A在執行線程B的start方法之前修改了共享變量的值,那么當線程B執行了start方法之時,線程A對共享變量的修改對線程B可見。
5. 傳遞性 A先於B,B先於C,那么A必然先於C
6. 線程終止原則 線程的所有操作先於線程的終結。Thread.join()方法的作用就是等待當前執行的線程的終止。
假設在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對共享變量的修改將對線程A可見。
7. 線程中斷規則
對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
8、對象終結規則 對象的構造函數執行,結束先於finalize()方法
指令重排發生在什么階段?
1. 編譯階段,字節碼編譯成機器指令碼階段。
2. CPU運行時,執行指令
volatile禁止重排優化 ---內存屏障(Memory Barrier)
下圖是JMM針對編譯器制定的volatile重排序規則表
指令重排code示例
/** * 並發場景下存在指令重排 */ public class VolatileReOrderSample { private static int x = 0, y = 0; private static volatile int a = 0, b =0; static Object object = new Object(); public static void main(String[] args) throws InterruptedException { int i = 0; for (;;){ i++; x = 0; y = 0; a = 0; b = 0; Thread t1 = new Thread(new Runnable() { public void run() { //由於線程one先啟動,下面這句話讓它等一等線程two. 讀着可根據自己電腦的實際性能適當調整等待時間. shortWait(10000); a = 1; //是讀還是寫?store,volatile寫 //storeload ,讀寫屏障,不允許volatile寫與第二步volatile讀發生重排 x = b; // 讀還是寫?讀寫都有,先讀volatile,寫普通變量 //分兩步進行,第一步先volatile讀,第二步再普通寫 } }, "t1"); Thread t2 = new Thread(new Runnable() { public void run() { b = 1; UnsafeInstance.reflectGetUnsafe().storeFence(); y = a; } }); t1.start(); t2.start(); t1.join(); t2.join(); /** * cpu或者jit對我們的代碼進行了指令重排? * 1,1 * 0,1 * 1,0 * 0,0 */ String result = "第" + i + "次 (" + x + "," + y + ")"; if(x == 0 && y == 0) { System.err.println(result); break; } else { System.out.println(result); } } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); } }
如果不要volatile去增加內存屏障?如何解決?
-- 手動增加屏障,通過Unsafe來解決.
loadFence() storeFence fulFence() .
Unsafe通過BootStwp被加載,否則拋異常。JVM的雙親委派機制
通過反射來獲取。
public class UnsafeInstance { public static Unsafe reflectGetUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { e.printStackTrace(); } return null; } }
內存屏障 Memory Barrier
1.寫寫storestore 2.寫讀storeload 3.讀寫loadstore 4.讀讀loadload
volatile禁止重排優化
volatile關鍵字另一個作用就是禁止指令重排優化,從而避免多線程環境下程序出現亂序 執行的現象,關於指令重排優化前面已詳細分析過,這里主要簡單說明一下volatile是如何實 現禁止指令重排優化的。先了解一個概念,內存屏障(Memory Barrier)。
內存屏障,又稱內存柵欄,是一個CPU指令,它的作用有兩個,一是保證特定操作的執行 順序,二是保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)。由於編譯 器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器 和CPU,
不管什么指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏 障禁止在內存屏障前后的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出 各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的新版本。總之, volatile變量正是通過內存屏障實現其在內存中的語義,即可見性和禁止重排優化。
下面看一 個非常典型的禁止重排優化的例子DCL,如下:
public class DoubleCheckLock { private static DoubleCheckLock instance; private DoubleCheckLock(){} public static DoubleCheckLock getInstance(){ //第一次檢測 if (instance==null){ //同步 synchronized (DoubleCheckLock.class) { if (instance == null){ //多線程環境下可能會出現問題的地方 instance = new DoubleCheckLock(); } } } return instance; } }
上述代碼一個經典的單例的雙重檢測的代碼,這段代碼在單線程環境下並沒有什么問題, 但如果在多線程環境下就可以出現線程安全問題。原因在於某一個線程執行到第一次檢測,讀 取到的instance不為null時,instance的引用對象可能沒有完成初始化。
總線風暴
問題:大量使用cas和volatile,會有什么問題? 高並發情況下為什么會產生總線風暴?
1. CAS ---> CPU工作內存與主內存產生大量的交互
2. volatile ---> 產生大量的無效的工作內存變量
解決辦法:
synchronized 關鍵字
單例設計---高並發(加雙重鎖)
public class Singleton { /** * 查看匯編指令 * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp */ private volatile static Singleton myinstance; public static Singleton getInstance() { if (myinstance == null) { synchronized (Singleton.class) { if (myinstance == null) { myinstance = new Singleton();//對象創建過程,本質可以分文三步 //對象延遲初始化 // } } } return myinstance; } public static void main(String[] args) { Singleton.getInstance(); } }
