在計算機在執行程式時,以指令為單位來執行,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到資料的讀取和寫入。
由於程式執行過程中的臨時資料是存放在主記憶體(實體記憶體)當中的,這時就存在一個問題,由於CPU執行指令的速度很快,而從記憶體讀取資料和向記憶體寫入資料的過程相對很慢,因此如果任何時候對資料的操作都要通過和記憶體的互動來進行,會大大降低指令執行的速度。因此就引入了快取記憶體。
特性:快取(Cache memory)是硬碟控制器上的一塊記憶體,是硬碟內部儲存和外界介面之間的緩衝器。
預讀取
相當於提前載入,猜測你可能會用到硬碟相鄰儲存地址的資料,它會提前進行載入到快取中,後面你需要時,CPU就不需要去硬碟讀取資料,直接讀取快取中的資料傳輸到記憶體中就OK了,由於讀取快取的速度遠遠高於讀取硬碟時磁頭讀寫的速度,所以能夠明顯的改善效能。
對寫入動作進行快取
硬碟接到寫入資料的指令之後,並不會馬上將資料寫入到碟片上,而是先暫時儲存在快取裡,然後傳送一個「資料已寫入」的訊號給系統,這時系統就會認為資料已經寫入,並繼續執行下面的工作,而硬碟則在空閒(不進行讀取或寫入的時候)時再將快取中的資料寫入到碟片上。
換到應用程式層面也就是,當程式在執行過程中,會將運算需要的資料從主記憶體複製一份到CPU的快取記憶體當中,那麼CPU進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料同步到主記憶體當中。
舉個簡單的例子,比如下面的這段程式碼:
i = i + 1;
當執行緒執行這個語句時,會先從主記憶體當中讀取i的值,然後複製一份到快取記憶體當中,然後CPU執行指令對i進行加1操作,然後將資料寫入快取記憶體,最後將快取記憶體中i最新的值重新整理到主記憶體當中。
這個程式碼在單執行緒中執行是沒有任何問題的,但是在多執行緒中執行就會有問題了(存在臨界區)。在多核CPU中,每條執行緒可能執行於不同的CPU中,因此每個執行緒執行時有自己的快取記憶體區(對單核CPU來說,其實也會出現這種問題,只不過是以執行緒排程的形式來分別執行的)。
比如有兩個執行緒像下列執行順序:
i = i + 1
,執行緒二執行var = i
i
,執行緒一隻是在快取記憶體中更新了變數,還未將變數i
寫會主記憶體i
不是最新值,此時多執行緒導致資料不一致 類似上面這種情況即為快取一致性問題。讀寫場景、雙寫場景都會存在快取一致性問題,但讀讀不會。前提是需要在多執行緒執行的環境下,並且需要多執行緒去存取同一個共用變數。
這裡的共用又可以回到上文中,即為上面所說,他們每個執行緒都有自己的快取記憶體區,但是都是從同一個主記憶體同步獲取變數。
那麼這種問題應該怎樣解決呢?
i = i + 1
整個命令過程中,其他執行緒是無法存取主記憶體的。 問題:執行緒為什麼會不安全?
答:共用資源不能及時同步更新,歸根於 分時系統 上下文切換時 指令還未執行完畢 (沒有寫回結果) 更新異常
眾所周知現在的網際網路大型專案,都是採用分散式架構同時具有其「三高症狀」,高並行、高可用、高效能。高並行為其中最重要的特性之一,在高並行場景下並行程式設計就顯得尤為重要,其並行程式設計的特性為原子性、可見性、有序性。
原子性指的是一個或多個操作要麼全部執行成功要麼全部執行失敗,期間不能被中斷,也不存在上下文切換,執行緒切換會帶來原子性的問題。
變數賦值問題:
b 變數賦值的底層位元組碼指令被分為兩步:第一步先定義 int b;第二步再賦值為 10。
兩條指令之間不具有原子性,且在多執行緒下會發生執行緒安全性問題
int b = 10;
可見性指的是當前執行緒對共用變數的修改對其他執行緒來說是可見的。以下案例中假設不會出現多執行緒原子性問題(比如多個執行緒寫入覆蓋問題等),即保證一次變數操作底層執行指令為原子性的。
例如上述變數在讀寫場景下,不能保證其可見性,導致寫執行緒完成修改指令時但為同步到主記憶體中,讀執行緒並不能獲得最新值。這就是對於B執行緒來說沒有滿足可見性。
案例解析:final關鍵字
final 變數可以保證其他執行緒獲取的該變數的值是唯一的。變數指成員變數或者靜態變數
b 變數賦值的底層位元組碼指令被分為兩步:第一步先定義 int b;第二步再賦值為 10
final a = 10; int b = 10;
final修飾的變數在其指令後自動加入了寫屏障,可以保證其變數的可見性
a 可以保證其他執行緒獲取的值唯一;b 不能保證其他執行緒獲取到的值一定是 10,有可能為 0。
讀取 final 變數解析 :
final 可以加強執行緒安全,而且符合物件導向程式設計開閉原則中的close,例如子類不可繼承、方法不可重寫、初始化後不可改變、非法存取(如修飾引數時,該引數為唯讀模式)等
有序性指的是程式執行的順序按照程式碼的先後順序執行。
在Java中有序性問題會時常出現,由於我們的JVM在底層會對程式碼指令的執行順序進行優化(提升執行速度且保證結果),這隻能保證單執行緒下安全,不能保證多執行緒環境執行緒安全,會導致指令重排發生有序性問題。
案例:排名世界第一的程式碼被玩壞了的單例模式
DCL(double checked):加入 volatile 保證執行緒安全,其實就是保證有序性。
上程式碼:其中包括了三個問題並且有詳細註釋解釋。(鳴謝itheima滿一航老師)
final class SingletonLazyVolatile {
private SingletonLazyVolatile() { }
// 問題1:為什麼加入 volatile 關鍵字?
// 答: 防止指令重排序 造成返回物件不完整。 如 TODO
private static volatile SingletonLazyVolatile INSTANCE = null;
// 問題2:對比實現3(給靜態程式碼塊加synchronized) 說出這樣做的意義?
// 答:沒有鎖進行判斷、效率較高
public static SingletonLazyVolatile getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
// 問題3:為什麼要在這裡加空判斷,之前不是判斷過了嗎?
// 答:假入t1 先進入判斷空成立,先拿到鎖, 然後到範例化物件這一步(未執行)
// 同時 執行緒 t2 獲取鎖進入阻塞狀態,若 t1 完成建立物件後,t2 沒有在同步塊這進行判空,t2 會再新建立一個物件,
// 導致 t1 的物件被覆蓋 造成執行緒不安全。
synchronized (SingletonLazyVolatile.class) { // t1
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new SingletonLazyVolatile(); // t1 這行程式碼會發生指令重排序,需要加入 volatile
// 如:先賦值指令INSTANCE = new SingletonLazyVolatile,導致範例不為空,下一個執行緒會判空失敗直接返回該物件
// 但是構造方法()指令還沒執行,返回的就是一個不完整的物件。
return INSTANCE;
}
}
}
通過對並行程式設計的三要素介紹,也就是說,要想並行程式正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程式執行不正確。
補充volatile知識:
volatile
只保證可見性(多執行緒下對變數的修改是可見的)、有序性(禁止進行指令重排序)
volatile 的底層實現原理是記憶體屏障(記憶體柵欄),Memory Barrier(Memory Fence),記憶體屏障會提供3個功能:
volatile
修飾之後的變數會加入讀寫屏障
寫屏障(sfence):保證在該屏障之前的,對共用變數的改動,都同步到主記憶體當中
讀屏障(lfence):保證在該屏障之後的, 對共用變數的讀取,載入的是主記憶體中的最新資料
對 volatile 變數的寫指令後會加入寫屏障
對 volatile 變數的讀指令前會加入讀屏障
關於volatile
的用途像兩階段終止、單例雙重鎖等等:
兩階段終止--volatile
@Log
public class TwoPhaseStop {
// 監控執行緒
private Thread monitorThread;
// 多執行緒共用變數 單執行緒寫入(停止執行緒) 多執行緒讀取 使用 volatile
private volatile boolean stop = false;
// 啟動監控執行緒
public void start() {
monitorThread = new Thread(() -> {
log.info("開始監控");
while (true) {
log.info("監控中");
Thread currentThread = Thread.currentThread();
if (stop) {
log.info("正在停止");
break;
}
try {
log.info("正常執行");
Thread.sleep(5000);
} catch (InterruptedException e) {
// sleep出現被打斷異常後、被打斷後會清除打斷標記
// 需要重新打斷標記
currentThread.interrupt();
}
}
log.info("已停止");
},"monitor");
monitorThread.start();
}
// 停止監控執行緒
public void stop() {
stop = true;
monitorThread.interrupt();
}
}
·
·
·
·
下篇預告:synchronized 和 volatile 區別和底層原理