CPU内部結構劃分
控制單元
運算單元
存儲單元
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
計算機多硬體多CPU結構:
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
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記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
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 ---> 産生大量的無效的工作記憶體變量
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則
解決辦法:
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();
}
}
第一節: JMM記憶體模型、CPU緩存一緻性原則(MESI)、volatile、指令重排、記憶體屏障(Memory Barrier)、as-if-serial、happen-before原則