理解JVM(五):Java記憶體模型與執行緒
Java記憶體模型
JMM(Java Memory Model)是JVM定義的記憶體模型,用來遮蔽各種硬體和作業系統的記憶體訪問差異。
* 主記憶體:所有的變數都儲存在主記憶體(Main Memory,類比實體記憶體)中。
* 工作記憶體:每條執行緒有自己的工作記憶體(Working Memory,類比處理器快取記憶體),執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。
記憶體間的互動操作
- lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。
- unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
- read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
- load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
- use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
- assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
- store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
- write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
其中read和load,store和write必須成對使用,順序但補一定連續的執行。通俗的說,就是執行了read,後面一定會執行load,但不一定read之後立馬load;store和write也一樣。lock和unlock也是成對出現,一個變數在同一時間點只能有一個執行緒對其進行lock。
對於普通變數的操作:
建立變數,是在主記憶體中初始化。
執行緒用到的變數,會先從主記憶體中拷貝(read
load
)到工作記憶體,然後引用(use
)變數並運算賦值(assign
)。然後儲存(store
)到工作記憶體,然後更新(write
)掉原來的變數。普通變數的值線上程間傳遞均需要通過主記憶體來完成。例如,執行緒A修改一個普通變數的值,然後向主記憶體進行回寫,另外一條執行緒B線上程A回寫完成了之後再從主記憶體進行讀取操作,新變數值才會對執行緒B可見。
對於
volatile
修飾的變數:過程和普通變數一樣。但保證變數對所有執行緒的可見性,並且會禁止指令重排序的優化。volatile的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。因此,可以說volatile保證了多執行緒操作時變數的可見性,而普通變數則不能保證這一點。
先行發生原則(happens-before)
它是判斷資料是否存在競爭、執行緒是否安全的主要依據,依靠這個原則,我們可以通過幾條規則一攬子地解決併發環境下兩個操作之間是否可能存在衝突的所有問題。
先行發生是Java記憶體模型中定義的兩項操作之間的偏序關係,如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了記憶體中共享變數的值、傳送了訊息、呼叫了方法等。
執行緒
執行緒是比程序更輕量級的排程執行單位,執行緒的引入,可以把一個程序的資源分配和執行排程分開,各個執行緒既可以共享程序資源(記憶體地址、檔案I/O等),又可以獨立排程(執行緒是CPU排程的基本單位)。
實現執行緒的3種方式:
- 使用核心執行緒實現
- 核心執行緒(Kernel-Level Thread,KLT)就是直接由作業系統核心(Kernel,下稱核心)支援的執行緒,這種執行緒由核心來完成執行緒切換,核心通過操縱排程器(Scheduler)對執行緒進行排程,並負責將執行緒的任務對映到各個處理器上。每個核心執行緒可以視為核心的一個分身,這樣作業系統就有能力同時處理多件事情,支援多執行緒的核心就叫做多執行緒核心(Multi-Threads Kernel)。
- 程式一般不直接使用核心執行緒,而是輕量級程序(通俗意義上的執行緒)。此2者
1:1
對應關係。建立,呼叫同步等都由系統執行,代價較高(需要在核心態和使用者態之間來回切換),每個輕量級程序會消耗一定的核心資源(如核心執行緒的棧空間),因此一個系統支援的輕量級程序時有限的。
- 使用使用者執行緒實現
- 廣義來說,一個執行緒只要不是核心執行緒,就可以認為是使用者執行緒。因此,輕量級程序也屬於使用者執行緒,但輕量級程序的實現始終是建立在核心之上的,許多操作都要進行系統呼叫,效率會受到限制。
- 狹義的說,使用者執行緒指的是完全建立在使用者空間的執行緒庫上,系統核心不能感知執行緒存在的實現。使用者執行緒的建立、同步、銷燬和排程完全在使用者態中完成,不需要核心的幫助。如果程式實現得當,這種執行緒不需要切換到核心態,因此操作可以是非常快速且低消耗的,也可以支援規模更大的執行緒數量,部分高效能資料庫中的多執行緒就是由使用者執行緒實現的。這種程序與使用者執行緒之間
1:N
的關係稱為一對多的執行緒模型。 - 使用使用者執行緒的優勢在於不需要系統核心支援,劣勢也是沒有系統核心的支援。所有的執行緒操作都需要使用者程式自己處理,實現會很複雜,所以現在很少使用了。
- 使用使用者執行緒加輕量級程序混合實現
- 既存在使用者執行緒,也存在輕量級程序。使用者執行緒還是完全建立在使用者空間中,因此使用者執行緒的建立、切換、析構等操作依然廉價,並且可以支援大規模的使用者執行緒併發。而作業系統提供支援的輕量級程序則作為使用者執行緒和核心執行緒之間的橋樑,這樣可以使用核心提供的執行緒排程功能及處理器對映,並且使用者執行緒的系統呼叫要通過輕量級執行緒來完成,大大降低了整個程序被完全阻塞的風險。在這種混合模式中,使用者執行緒與輕量級程序的數量比是不定的,即為
N:M
的關係,這種就是多對多的執行緒模型。
- 既存在使用者執行緒,也存在輕量級程序。使用者執行緒還是完全建立在使用者空間中,因此使用者執行緒的建立、切換、析構等操作依然廉價,並且可以支援大規模的使用者執行緒併發。而作業系統提供支援的輕量級程序則作為使用者執行緒和核心執行緒之間的橋樑,這樣可以使用核心提供的執行緒排程功能及處理器對映,並且使用者執行緒的系統呼叫要通過輕量級執行緒來完成,大大降低了整個程序被完全阻塞的風險。在這種混合模式中,使用者執行緒與輕量級程序的數量比是不定的,即為
Java執行緒實現:JDK1.2之前是使用者執行緒,1.2和之後的版本,使用作業系統原生執行緒模型(核心執行緒)。
Java執行緒排程
執行緒排程是指系統為執行緒分配處理器使用權的過程,主要排程方式有兩種:
* 協同式執行緒排程(Cooperative Threads-Scheduling):執行緒的執行時間由執行緒本身來控制,執行緒把自己的工作執行完了之後,要主動通知系統切換到另外一個執行緒上。
* 好處:實現簡單,而且由於執行緒要把自己的事情幹完後才會進行執行緒切換,切換操作對執行緒自己是可知的,所以沒有什麼執行緒同步的問題。
* 壞處:執行緒執行時間不可控制,甚至如果一個執行緒編寫有問題,一直不告知系統進行執行緒切換,那麼程式就會一直阻塞在那裡。
* 搶佔式執行緒排程(Preemptive Threads-Scheduling):每個執行緒將由系統來分配執行時間,執行緒的切換不由執行緒本身來決定(在Java中,Thread.yield()
可以讓出執行時間,但是要獲取執行時間的話,執行緒本身是沒有什麼辦法的)。在這種實現執行緒排程的方式下,執行緒的執行時間是系統可控的,也不會有一個執行緒導致整個程序阻塞的問題,Java使用的執行緒排程方式就是搶佔式排程 。
雖然Java執行緒排程是系統自動完成的,但是我們還是可以“建議”系統給某些執行緒多分配一點執行時間,另外的執行緒少分配一點——通過設定執行緒優先順序的方式(兩個執行緒同時處於Ready狀態時,優先順序越高的執行緒越容易被系統選擇執行),不過這方法不是很可靠,因為系統執行緒優先順序和Java的10種執行緒優先順序不一定一一對應。
執行緒狀態
在任意時間點,一個執行緒只有一種狀態
* 新建(New):建立後尚未啟動
* 執行(Runable):正在執行或正在等待CPU為它分配執行時間
* 等待(Waiting):
* 無限等待(Waiting):執行緒不會被分配CPU執行時間,等待被其他執行緒顯式地喚醒。
* 期限等待(Timed Waiting):執行緒不會被分配CPU執行時間,無須等待被其他執行緒顯式地喚醒,在一定時間後它們會由系統自動喚醒。
* 阻塞(Blocked):被阻塞
* 阻塞和等待的區別:阻塞狀態
在等待著獲取到一個排他鎖,這個事件將在另外一個執行緒放棄這個鎖的時候發生;而等待狀態
則是在等待一段時間,或者喚醒動作的發生。在程式等待進入同步區域的時候,執行緒將進入這種狀態。
* 結束(Terminated):已終止