Java內存模型與線程

隨着多核CPU的高速發展,爲了充分利用硬件的計算資源,操作系統的併發多任務功能正變得越來越重要,但是CPU在進行計算時,還需要從內存讀取輸出,並將計算結果存放到內存中,然而由於CPU的運算速度比內存高几個數量級,CPU內的寄存器數量和容量有限,爲了不讓CPU長時間處於等待內存的空閒狀態,在CPU和內存之間引入了速度接近CPU的高速緩存Cache作爲CPU和內存之間的緩衝。計算機硬件併發的原理如下:

Java虛擬機對併發的支持類似於計算機硬件,java虛擬機的併發支持是通過java虛擬機的內存模型來實現的。Java虛擬機的內存模型分爲主內存和工作內存,程序中所有的變量都存儲在主內存中,每個線程有自己的私有工作內存,工作內存中保存了被該線程使用到的變量的主內存拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量,不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成。Java虛擬機併發原理如下:


Java虛擬機內存模型中定義了8種關於主內存和工作內存的交互協議操作:

(1).lock鎖定:作用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。

(2).unlock解鎖:作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量可以被其他線程鎖定。

(3).read讀取:作用於主內的變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用。

(4).load加載:作用於工作內存的變量,把read讀取操作從主內存中得到的變量值放入工作內存的變量拷貝中。

(5).use使用:作用於工作內存的變量,把工作內存中一個變量的值傳遞給java虛擬機執行引擎,每當虛擬機遇到一個需要使用到變量值的字節碼指令時將會執行該操作。

(6).assign賦值:作用於工作內存變量,把一個從執行引擎接收到的變量的值賦值給工作變量,每當虛擬機遇到一個給變量賦值的字節碼時將會執行該操作。

(7).store存儲:作用於工作內存的變量,把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用。

(8).write寫入:作用於主內存的變量,把store操作從工作內存中得到的變量值放入主內存的變量中。

Java內存模型對上述8種操作有如下的約束:

(1).把一個變量從主內存複製到工作內存中必須順序執行read讀入操作和load載入操作。

把一個變量從工作內存同步回主內存中必須順序執行store存儲操作和write寫入操作。

read和load操作之間、store和write操作之間可以插入其他指令,但是read和load操作、store和write操作必須要按順序執行,即不允許read和load、store和write操作之一單獨出現。

(2).不允許一個線程丟棄它的最近的assign賦值操作,即工作內存變量值改變之後必須同步回主內存。只有發生過assign賦值操作的變量才需要從工作內存同步回主內存。

(3).一個新變量只能在主內存中產生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,即一個變量在進行use和store操作之前,必須先執行過assgin和load操作。

(4).一個變量在同一時刻只允許一條線程對其進行lock鎖定操作,但是lock鎖定可以被一條線程重複執行多次,多次執行lock之後,只有執行相同次數的unlock操作變量纔會被解鎖。

(5).如果對一個變量執行lock鎖定操作,將會清空工作內存中該變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。

(6).如果一個變量事先沒有被lock鎖定,則不允許對這個變量進行unlock解鎖操作,也不允許對一個被別的線程鎖定的變量進行unlock解鎖。

(7).一個變量進行unlock解鎖操作之前,必須先把此變量同步回主內存中(執行store和write操作)。

Java中的關鍵字volatile是java虛擬機提供的最輕量級的線程同步機制,當一個變量被聲明爲volatile之後,該變量將具備以下兩種特性:

(1).volatile保證變量對所有線程的可見性,即任何一個線程修改了該變量的值之後,新值對於所有其他線程都是可以立即得知的。

而普通變量需要先將工作內存中的變量同步回主內存,其他線程都需要從主內存重新讀取變量的值才能使用最新修改後的值。

volatile變量也可以在各個工作內存中存在不一致的情況,但由於每次使用之前都需要先刷新(工作內存變量重新執行初始化),執行引擎看不到變量不一致的情況,因此可以任務volatile變量不存在不一致的情況。

但是java中的運算並非全部都是原子操作,因此volatile變量的運行在併發下一樣是線程不安全的。

由於volatile變量只能保證可見性,只有在符合如下兩條規則情況纔是線程安全的。

a.運算結果不依賴變量的當前值,或者能夠確保只有單一線程修改變量的值。

b.變量不需要與其他其他變量共同參與不變約束。

不符合上述兩條規則情況下,仍然需要通過synchronized同步關鍵字或者加鎖機制來保證線程安全。

(2).volatile禁止指令重排序優化。

普通變量僅能保證在方法執行過程中所有依賴賦值結果的地方都能獲取正確的結果,而無法保證變量賦值操作順序與程序代碼執行順序一致。

volatile禁止指令重排序,因此volatile變量的約束如下:

a.volatile變量的操作必須按read->load->use順序,即每次在工作內存中使用變量前必須先從主內存中刷新最新的值,以保證能看到其他線程對變量的最新修改。

b. volatile變量的操作必須按assign->store->write順序,即每次在工作內存爲變量賦值之後必須將變量的值同步回主內存,以保證讓其他線程能看到變量的最新修改。

c.若線程對volatile變量A的assign或者use操作先於對volatile變量B的assign或者use操作,則線程對volatile變量A的read/load或者store/write操作也必定先於對volatile變量B的read/load或者store/write操作。


Java的併發編程是依賴虛擬機內存模型的三個特性實現的:

(1).原子性(Atomicity):

原子性是指不可再分的最小操作指令,即單條機器指令,原子性操作任意時刻只能有一個線程,因此是線程安全的。

Java內存模型中通過read、load、assign、use、store和write這6個操作保證變量的原子性操作。

long和double這兩個64位長度的數據類型java虛擬機並沒有強制規定他們的read、load、store和write操作的原子性,即所謂的非原子性協定,但是目前的各種商業java虛擬機都把long和double數據類型的4中非原子性協定操作實現爲原子性。所以java中基本數據類型的訪問讀寫是原子性操作。

對於大範圍的原子性保證需要通過lock和unlock操作以及synchronized同步塊來保證。

(2).可見性(Visibility):

可見性是指當一個線程修改了共享變量的值,其他線程可以立即得知這個修改。

Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。

Java中通過volatile、final和synchronized這三個關鍵字保證可見性:

volatile:通過刷新變量值確保可見性。

synchronized:同步塊通過變量lock鎖定前必須清空工作內存中變量值,重新從主內存中讀取變量值,unlock解鎖前必須把變量值同步回主內存來確保可見性。

final:被final修飾的字段在構造器中一旦被初始化完成,並且構造器沒有把this引用傳遞進去,那麼在其他線程中就能看見final字段的值,無需同步就可以被其他線程正確訪問。

(3).有序性(Ordering):

線程的有序性是指:在線程內部,所有的操作都是有序執行的,而在線程之間,因爲工作內存和主內存同步的延遲,操作是亂序執行的。

Java通過volatile和synchronized關鍵字確保線程之間操作的有序性。

volatile禁止指令重排序優化實現有序性。

synchronized通過一個變量在同一時刻只允許一個線程對其進行lock鎖定操作來確保有序性。

JDK線程的實現如下:

(1).Kernal thread:KLT,內核線程,運行在內核態,是直接有操作系統內核支持的線程,有操作系統內核完成內核線程切換,內核操作線程調度器Threadscheduler對內核線程進行調度,負責將內核線程任務映射到各個處理器上。

(2).Light weight process: LWP,輕量級用戶進程,是編程中傳統意義上的線程,每個輕量級進程都由一個內核線程支持。

(3).User thread:UT,用戶線程,運行在用戶態,完全由用戶空間線程庫實現,內核線程無法感知到用戶線程的實現,用戶線程的創建、同步、調度和銷燬完全在用戶態中完成,不需要內核態的支持。

JDK的線程是基於操作系統原生線程模型來實現的,因此JDK版本中線程模型取決於java虛擬機線程與操作系統線程的映射,在不同平臺上是不同的。

線程調度有兩種方式:

(1).協同式:線程的執行時間由線程本身來控制,線程任務執行完成之後主動通知系統切換到另一個線程去執行。

優點:實現簡單,線程切換操作對線程本身是可知的,不存在線程同步問題。

缺點:線程執行時間不可控制,如果線程長時間執行不讓出CPU執行時間可能導致系統崩潰。

(2).搶佔式:每個線程的執行時間有操作系統來分配,操作系統給每個線程分配執行的時間片,搶到時間片的線程執行,時間片用完之後重新搶佔執行時間,線程的切換不由線程本身來決定。

優點:線程執行時間可控制,不會因爲一個線程阻塞問題導致系統崩潰。

 

當前JDK的多線程是搶佔式的多線程系統,但是可以通過設置線程優先級和改變線程的執行時間分配概率。

注意:由於JDK的線程優先級和操作系統的線程優先級不是一一對應的,因此建議只使用1(最低優先級)、5(正常優先級)和10(最高優先級)這三個優先級。

另外,線程優先級只是操作系統給線程分配執行時間的概率大小,不是絕對的。

Java中線程的狀態即調度關係如下:

發佈了36 篇原創文章 · 獲贊 21 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章