Java記憶體模型與執行緒知識點總結
首先討論一下物理機對於併發的處理方案
運算任務不可能只靠處理器簡單的計算就能完成,必須還要增加與記憶體的互動操作(如讀取資料,儲存資料), 由於計算機的儲存裝置與處理器的運算速度之間有著幾個數量級的差距,所以現代計算機系統選擇加入快取記憶體(Cache)來進行記憶體與處理器之間的快取來提高效率
由於快取記憶體是與處理器繫結的,而主記憶體是所有處理共享的,所以會產生快取一致性的問題,意思是當多個處理器都要處理同一塊主記憶體區域時,可能會導致處理器的快取中資料與主記憶體中的資料不一致,如何判斷和選擇正確的資料呢? 為了解決這個問題就要用到快取一致性的協議
幾個概念:
亂序執行優化 與 指令重排序:將不存在資料依賴關係的指令按照非線性順序執行,比如泡茶這個操作會包括 洗杯子,燒開水,倒入茶葉,泡茶几個步驟,其中燒開水和洗杯子可以是同時進行的,那麼就可以將整個流程劃分為兩個分隔的流程, 洗杯子,倒茶葉,泡茶交給一個處理器,燒開水交給另一個處理器來完成。 指令重排序就是Java編譯器中存在的類似處理器進行的亂序重拍的優化。
資料依賴: 如果兩個操作需要訪問同一個變數並且兩個操作之中有一個是寫操作,那麼就可成為兩個操作之間存在資料依賴關係。
as-if-serial語義: 無論是處理器還是編譯器,不管怎麼重排都要保證(單執行緒)程式的執行結果不能被改變,這就是as-if-serial語義.比如燒水煮茶的最終結果永遠是煮茶,而不能變成燒水,順序可以不同,但是結果要相同。
Java記憶體模型是為了遮蔽掉各種硬體和作業系統的記憶體訪問差異,從而實現Java程式在各種平臺之下都能夠達到一致的併發效果。
設計的主要物件是各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素等共有元素)的訪問規則(在工作記憶體和主記憶體之間的相互訪問規則)。
Java記憶體模型與Java記憶體區域劃分的關係: 這是兩個不同層次上的劃分, 只能勉強對應。
Java記憶體區域劃分是虛擬機器在執行Java程式的過程之中把它所管理的記憶體按照不同的用途劃分為不同的資料區域: 主要分為堆(執行緒私有),方法區(執行緒私有),Native方法棧, 虛擬機器棧,程式計數器。
- 記憶體間互動基本操作:
- 主記憶體相關:
- 鎖定相關: lock(作用於主記憶體的變數) 將變數標記為執行緒獨佔狀態, unlock(作用於主記憶體的變數) 解除變數執行緒獨佔的狀態,
- 與工作記憶體互動相關:read(作用於主記憶體的變數) 將變數從主記憶體傳到工作記憶體之中, write(與load反向)
- 工作記憶體相關:
- 與主記憶體互動相關: load(將read到的變數放入工作記憶體中的變數副本), store(與read相反操作)
- 工作記憶體變數相關: use(將工作記憶體的值給執行引擎), assign(將執行引擎的值給工作記憶體)
volatile關鍵字
volatile可以被看做為Java虛擬機器提供的最輕量級的同步機制,只能保證特定場景下的執行緒安全:
1.運算結果不依賴變數的當前值,能夠保證只有單一的執行緒修改變數的值
2.變數不需要與其他的狀態變數共同參與不變約束
volatitle boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
兩個主要特性:
- 保證被修飾的變數對所有執行緒的可見性(可見性含義是當一個執行緒修改了volatile變數的值,新值對於其它執行緒來說可以立即得知, 通過每次使用之前都需要重新整理該變數來保證這一點 對比普通變數: 變數值線上程之間傳遞是通過主記憶體來完成的, A執行緒修改一個普通變數的值,然後向主記憶體進行回寫,另外一個執行緒B在A回寫完成之後再從主記憶體進行讀取時,B執行緒才能夠讀取新變數的值)但不能夠保證執行緒安全
- 禁止指令重排序優化: 與 普通變數僅能夠保證在方法的執行過程之中所有依賴賦值記過的地方都能得到正確的結果, 而不能夠保證賦值操作的順序與程式程式碼中的執行順序一致。
原子性: 直接保證原子性的變數操作為read, load, assign, use, store, write六個
可見性: 當一個執行緒修改了共享變數的值, 其他執行緒能夠立即得知這個修改。 可以通過volatitle, synchronized, final關鍵字保證。
有序性: 本執行緒內觀察,可以看出全部有序。 而從另一個執行緒內觀察,可以看出全部無序. 通過volatile 和 synchronized關鍵字來保證執行緒之間的有序。 synchronized保證在同一個時刻只允許一個執行緒對其進行lock操作。
Java中的執行緒實現,三種方式:
- 使用核心執行緒
- 使用使用者執行緒
- 混合使用
Java執行緒排程
分為兩種方式: 協同式執行緒排程(Cooperative Threads-Scheduling) 和搶佔式執行緒排程(Preemptive Threads-Scheduling)
協同式: 執行緒的執行時間由執行緒本身來控制, 當所有工作執行完了之後,通知系統切換到另一個執行緒上, 好處實現簡單,不存線上程同步的問題. 問題是: 執行緒執行時間不可控, 會導致程式一直被某一個執行緒的執行阻塞, 可能會與系統崩潰的危險。
搶佔式(Java採用): 每個執行緒的執行時間由系統來分配, 執行緒的切換不由執行緒本身決定。 可以通過優先順序進行一些優化, 10個級別, 但是優先順序與不同系統的底層執行緒優先順序可能會存在衝突
Java執行緒轉換:
新建(New): 建立之後還未啟動的執行緒
執行(Runnable): 分為Running(正在執行) 和 Ready(正在等待CPU為它分配執行時間) 兩種狀態
等待(Waiting) 可以分為無限期等待 Waiting(不會被分配CPU執行時間, 必須要等待其它執行緒顯式的喚醒) 和限期等待(Timed Waiting(不會被分配CPU執行時間,但不需要等待被其它執行緒顯式喚醒,會在一定時間之後由系統自動進行喚醒) 對應方法Thread.sleep() 設定了Timeout引數的Object.wait() 設定了Timeout引數的Thread.join()
阻塞(Blocked): 等待著獲取一個排它鎖。
結束(Terminated): 終止執行緒的執行, 終止之前要完成所有執行緒任務的執行 (happen before原則)