深入瞭解jvm-2Edition-Java記憶體模型與執行緒
1、概述
現代計算機中,CPU的運算速度遠遠快於記憶體存取速度,為了更好利用CPU的硬體效能,人們想出來很多辦法。
1、減小CPU和記憶體的速度差距——cache
通過在CPU和記憶體之間加入一層緩衝層,CPU的輸出輸入到cache中,輸入從cache中獲得。
然後再利用區域性性原理,預先在cache中存入更多資料,來滿足CPU的高速處理。
cache命中率、cache缺失、cache一致性、寫緩衝、寫回、寫直達。
2、減少CPU等待——多工處理
在當前任務在執行IO操作時,採用DMA方式執行IO,CPU儲存當前程序的工作現場,並切換到其他程序執行。
在多處理器的CPU中,每個處理器都有自己的快取記憶體,這時就引出了一個問題——快取一致性(記憶體可見性)。
當多個處理器都對同一資料進行處理時,資料在處理器的快取中可能會產生多個副本。
Java虛擬機器規範中試圖定義一種統一的Java記憶體模型來遮蔽掉硬體和作業系統記憶體訪問的差異,從而在記憶體訪問上實現平臺無關性。
Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體取出的底層細節。
2、Java記憶體模型
2.1 主記憶體與工作記憶體
Java記憶體模型規定所有的變數都儲存在主記憶體中。
每個執行緒有自己的工作記憶體,其中儲存了該執行緒使用到的變數的主記憶體副本拷貝(引用)。
執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。
執行緒間的變數值的傳遞均需要通過主記憶體來完成。
2.2 記憶體間互動操作
如何在主記憶體和工作記憶體之間傳遞資料?
Java記憶體模型規定了8種操作:
1、lock
作用於主記憶體的變數,把變數標識為一條執行緒獨佔的狀態。
2、unlock
將主記憶體變數從鎖定狀態釋放。釋放後的變數才能被其他執行緒鎖定。
3、read
作用於主記憶體變數,把變數值從主記憶體傳輸入工作記憶體,以便後面的load操作使用。
4、load
作用於工作記憶體的變數,把read操作輸入的值放入工作記憶體的變數副本中。
5、use
作用於工作記憶體變數,將工作記憶體變數值傳遞給執行引擎(當虛擬機器執行到需要變數值的指令時)。
6、assign
作用於工作記憶體變數,把從執行引擎收到的值賦給工作記憶體變數。
7、store
作用於工作記憶體變數,把變數值傳送到主記憶體中。以便後面的write指令使用。
8、write作用於主記憶體變數,把store操作傳過來的值放入主記憶體的變數中。
執行以上8種操作需要滿足以下規則:
1、read和load,store和write必須配對出現。不能存在輸入但不接收的情況。
2、不允許一個執行緒丟棄它最近的assign操作,變數在工作記憶體中改變後必須同步回主記憶體。
3、不允許一個執行緒無原因(沒有改變)地將資料同步回主記憶體。
4、新的變數只能在主記憶體中產生,使用變數前,必須經過assign和load操作。
5、一個變數同一時刻只能有一個執行緒對其進行lock操作。但是,可以被一個執行緒多次執行lock操作(重入)。
6、如果對變數執行lock操作,那麼會清空工作記憶體中變數的值,使用前需要重新load或賦值。
7、unlock必須在lock之後才能進行。
8、對變數執行unlock之前,必須同步回記憶體。
主記憶體--->工作記憶體:read--->load 按先後順序執行,沒有原子性要求。
工作記憶體--->主記憶體:store--->write 按先後順序執行,沒有原子性要求。
對於volatile型別變數的特殊規則:
1、可見性
一個執行緒修改了變數值,其他執行緒立即可以知道這個修改。
沒有保證原子性。因此,read->change->write的操作(依賴當前值)是不安全的。
2、禁止指令重排序
在原生代碼中插入了許多記憶體屏障來保證處理器不發生亂序執行。
對於long、double型別的特殊規則:
非原子協定:對於64位資料型別的讀寫可以採用兩條32位指令來實現,且不強制兩條指令的原子性。
2.3 先行發生規則
3、Java與多執行緒
執行緒是比程序更輕量級的排程執行單位,執行緒的引入,可以把程序的資源分配和排程執行分開。
各個執行緒既可以共享程序資源(記憶體、IO),又可以獨立的被排程執行。
3.1 執行緒實現
1、使用核心執行緒實現
核心執行緒(Kernal-Level Thread):直接由作業系統核心支援和管理的執行緒。
輕量級程序(Light Weight Process):核心執行緒的介面,用於程式呼叫。
使用核心執行緒實現就是在程式中呼叫輕量級程序介面,將執行緒的實現交給作業系統。
LWP:KLT=1:1。
執行緒切換代價太高,支援的執行緒數量有限。
2、使用使用者執行緒實現
自己在程式中實現執行緒的建立、同步、銷燬和排程。
切換效率高,實現複雜。
3、使用使用者執行緒+核心執行緒 的混合模式
建立、切換銷燬自己實現,排程和處理器對映則依賴作業系統。
UT(user thread):LWP:KLT=m:n:n。
折中。
3.2 Java執行緒實現
Linux和windows都是1:1的實現。
4、Java執行緒排程
搶佔式和非搶佔式。
Java使用搶佔式。
執行緒優先順序,Java有10個等級,但是最終都要對映到作業系統的優先順序上。
Windows有7個等級。
Java執行緒狀態:
1、New
建立後尚未啟動。
2、Runable
包括作業系統中的Running和Ready狀態。
3、Waiting
無限期地等待,直到被顯示地喚醒。
沒有設Timeout引數的Object.wait()方法和Thread.join()方法;
LockSupport.park() 方法。
4、Timed Waiting
一段時間後自動甦醒。
Thread.sleep()方法;
設定了Timeout引數的Object.wait和Thread.join方法;
LockSupport.parkNanos()方法;
LockSupport.parkUtil()方法。
5、Blocked
6、Terminated