1. 程式人生 > 其它 >Java多執行緒程式設計指南 核心篇 讀書筆記

Java多執行緒程式設計指南 核心篇 讀書筆記

volatile CAS final static
原子性保障 具備 具備 具備 不涉及 不涉及
可見性保障 具備 具備 不具備 不具備 具備①
有序性保證 具備 具備 不涉及 具備 具備②
上下文切換 可能 具備 不會 不會 可能③
備註 被徵用的鎖可能導致上下文切換 僅能夠保障對volatile變數的讀/寫操作本身的原子性 ①②僅在一個執行緒初次讀取一個類的靜態變數時起作用
③靜態變數所屬類的初始化可能導致上下文切換

鎖是Java平臺中功能最強大的一種執行緒同步機制,同時其開銷也最大,可能導致的問題也最多。被爭用的鎖會導致上下文切換,鎖還可能導致死鎖、鎖死等執行緒活性故障。鎖適用於存在多個執行緒對多個共享資料進行更新、check-then-act操作或者read-modify-write操作這樣的場景。

鎖的排他性以及Java 虛擬機器在臨界區前後插入的記憶體屏障使得臨界區中的操作具有原子性。由此,鎖還保障了寫執行緒在臨界區中執行操作在讀執行緒看來是有序的,即保障了有序性。Java 虛擬機器在 MonitorExit對應的機器碼後插入的記憶體屏障則保障了可見性。鎖能夠保障執行緒安全的前提是訪問同一組共享資料的多個執行緒必須同步在同一個鎖之上,否則原子性、可見性和有序性均無法得以保障。在滿足貌似序列語義的前提下,臨界區內以及臨界區外的操作可以在各自範圍內重排序。臨界區外的操作可能會被JIT編譯器重排到臨界區內、但是臨界區內的操作不會被編譯器、處理器重排到臨界區之外。

Java中的所有鎖都是可重入的。內部鎖( synchronized )僅支援非公平鎖,因此它可能導致飢餓。而顯式鎖(ReentrantLock )既支援非公平鎖又支援公平鎖,顯式鎖可能導致鎖洩漏。內部鎖和顯式鎖各有所長,各有所短。讀寫鎖(ReadWriteLock )由於其內部實現的複雜性,僅適用於只讀操作比更新操作要頻繁得多且讀執行緒持有鎖的時間比較長的場景。讀寫鎖( ReadWriteLock )中的讀鎖和寫鎖是一個鎖例項所充當的兩個角色,並不是兩個獨立的鎖。

執行緒轉儲中可以包含鎖的相關資訊——執行緒在等待哪些鎖,這些鎖又是被哪些執行緒持有的。

volatile相當於輕量級鎖。線上程安全保障方面與鎖相同的是,volatile能夠保障可見性、有序性;與鎖不同的是 volatile不具有排他性,也不會導致上下文切換。與鎖類似,Java虛擬機器實現volatile對有序性和可見性的保障也是藉助於記憶體屏障。從這個角度來看,volatile變數寫操作相當於釋放鎖,volatile變數讀操作相當於獲得鎖--—-Java虛擬機器通過在volatile變數寫操作之前插入一個釋放屏障,在volatile變數讀操作之後插人一個獲取屏障這種成對的釋放屏障和獲取屏障的使用實現了volatile對有序性的保障。類似地,Java虛擬機器在volatile變數寫操作之後插入一個儲存屏障,在volatilc變數讀操作之前插人一個載入屏障這種成對的儲存屏障與載入屏障的使用實現了volatile對可見性的保障。

在原子性方面,volatile僅能夠保障long/double型變數寫操作的原子性。如果要保障對volatile變數的賦值操作的執行緒安全,那麼賦值操作右邊的表示式不能涉及任何共享變數(包括被賦值的變數本身)。volatile關鍵字在可見性、有序性和原子性方面的保障並不會對其修飾的陣列的陣列元素的讀、寫操作起作用。
volatile變數寫操作的成本介於普通變數的寫操作和在臨界區內進行的寫操作之間。讀取一個volatile變數總是意味著(通過快取記憶體進行的)讀記憶體操作,而不是從暫存器中讀取。因此,volatile變數讀操作的成本比讀取普通變數要略高一些,但比在臨界區中讀取變數要低。
volatile的典型運用場景包括:一,使用volatile變數作為狀態標誌;二,使用volatile保障可見性;三,使用volatile變數替代鎖;四,使用volatile實現簡易版讀寫鎖。

CAS使得我們可以在不借助鎖的情況下保障read-modify-write操作、check-then-act操作的原子性,但是它並不保障可見性,原子變數類相當於基於CAS實現的增強型volatile變數(保障volatile無法保障的那一部分操作的原子性)。常用的原子變數類包括AtomicInteger、AtomicLong、AtomicBoolean等。AtomicStampedReference則可以用於規避CAS的ABA問題。
static關鍵字能夠保證一個執行緒即使在未使用其他同步機制的情況下也總是可以讀取到一個類的靜態變數的初始值(而不是預設值)。對於引用型靜態變數,static還確保了該變數引用的物件已經初始化完畢。但是,static的這種可見性和有序性保障僅在一個執行緒初次讀取靜態變數的時候起作用。

final關鍵字在多執行緒環境下也有其特殊作用:當一個物件被髮布到其他執行緒的時候,該物件的所有final欄位(例項變數)都是初始化完畢的。而非final欄位沒有這種保障,即這些執行緒讀取該物件的非final欄位時所讀取到的值可能仍然是相應欄位的預設值。對於引用型final欄位,final關鍵字還進一步確保該欄位所引用的物件已經初始化完畢。
實現物件的安全釋出,通常可以依照以下順序選擇適用且開銷最小的執行緒同步機制。·使用static關鍵字修飾引用該物件的變數。
·使用final關鍵字修飾引用該物件的變數。使用volatile關鍵字修飾引用該物件的變數。·使用AtomicReference來引用該物件。
·對訪問該物件的程式碼進行加鎖。

為避免將this代表的當前物件逸出到其他執行緒,我們應該避免在構造器中啟動工作者執行緒。通常我們可以定義一個init方法,在該方法中啟動工作者執行緒。在此基礎上,定義一個工廠方法來建立(並返回)相應的例項,並在該方法中呼叫該例項的init方法。

Object.wait()/notify()的內部實現

我們知道Java虛擬機器會為每個物件維護一個入口集(Entry Set)用於儲存中請該物件內部鎖的執行緒。此外,Java虛擬機器還會為每個物件維護一個被稱為等待集(Wait Set )的佇列,該佇列用於儲存該物件上的等待執行緒。Object.wait()將當前執行緒暫停並釋放相應內部鎖的同時會將當前執行緒(的引用)存入該方法所屬物件的等待集中。執行一個物件的notify方法會使該物件的等待集中的一個任意執行緒被喚醒。被喚醒的執行緒仍然會停留在相應物件的等待集之中,直到該執行緒再次持有相應內部鎖的時候(此時Object.wait()呼叫尚未返回) Object.wait()會使當前執行緒從其所在的等待集移除,接著Object.wait()呼叫就返回了。Object.wait()/notify()實現的等待/通知中的幾個關鍵動作,包括將當前執行緒加入等待集、暫停當前執行緒、釋放鎖以及將喚醒後的等待執行緒從等待集中移除等,都是在Object.wait()中實現的。Object.wait()的部分內部實現相當於如下虛擬碼:

等待執行緒在語句①被執行之後就被暫停了。被喚醒的執行緒在其佔用處理器繼續執行的時候會繼續執行其暫停前呼叫的Object.wait()中的其他指令,即從上述程式碼中的語句②開始繼續執行:先再次申請Object.wait()所屬物件的內部鎖,接著將當前執行緒從相應的等待集中移除,然後Object.wait()呼叫才返回!

Java執行時儲存空間

瞭解Java執行時儲存空間的有關知識有助於我們更好地理解多執行緒程式設計。Java執行時( Java Runtime)空間可以分為堆(Heap)空間、棧( Stack)空間和非堆(Non-Heap )空間。其中,堆空間和非堆空間是可以被多個執行緒共享的,而棧空間則是執行緒的私有空間,每個執行緒都有其棧空間,並且一個執行緒無法訪問其他執行緒的棧空間。

堆空間( Heap space)用於儲存物件,即建立一個例項的時候該例項所需的儲存空間是在堆空間中進行分配的,堆空間本身是在Java虛擬機器啟動的時候分配的一段可以動態擴容的記憶體空間。因此,類的例項變數是儲存在堆空間中的。由於堆空間是執行緒之間的共享空間、因此例項變數以及引用型例項變數所引用的物件是可以被多個執行緒共享的。不管引用物件的變數的作用域如何(區域性變數、例項變數和靜態變數),物件本身總是儲存在堆空間中的。堆空間也是垃圾回收器( Garbage Collector)工作的場所.即堆空間中沒有可達引用的物件(不再被使用的物件)所佔用的儲存空間會被垃圾回收器回收。堆空間通常可以進一步劃分為年輕代( Young Generation)和年老代( Old/Tenured Generation )。物件所需的儲存空間是在年輕代中進行分配的。垃圾回收器對年輕代中的物件進行的垃圾回收被稱為次要回收(Minor Collection )。次要回收中“倖存”下來(即沒有被回收掉)的物件最終可能被移人(改變物件所在的儲存空間)年老代。垃圾回收器對年老代中的物件進行的垃圾回收被稱為主要回收( Major Collection ).

棧空間(Stack Space)是為執行緒的執行而準備的一段固定大小的記憶體空間,每個執行緒都有其棧空間'。棧空間是線上程建立的時候分配的。執行緒執行(呼叫)一個方法前,Java虛擬機器會在該執行緒的棧空間中為這個方法呼叫建立一個棧幀( Frame )。棧幀用於儲存相應方法的區域性變數、返回值等私有資料。可見,區域性變數的變數值儲存在棧空間中。基礎型別( Primitive Type)變數和引用型別(Reference Type)變數的變數值都是直接儲存在棧幀中的*。引用型變數的值相當於被引用物件的記憶體地址,而引用型變數所引用的物件仍然在堆空間中。也就是說,對於引用型區域性變數,棧幀中儲存的是相應物件的記憶體地址而不是物件本身!由於一個執行緒無法訪問另外一個執行緒的棧空間,因此,執行緒對區域性變數以及對只能通過當前執行緒的區域性變數才能訪問到的物件進行的操作具有固有( Inherent〉的執行緒安全性。
非堆空間( Non-Heap Space)用於儲存常量以及類的元資料(Meta-data)等,它也是在Java虛擬機器啟動的時候分配的一段可以動態擴容的記憶體空間。類的元資料包括類的靜態變數、類有哪些方法以及這些方法的元資料(包括名稱、引數和返回值等)。非堆空間也是多個執行緒之間共享的儲存空間。類的靜態變數在非堆空間中的儲存方式與區域性變數在棧空間的儲存方式相似,即這些空間中僅儲存變數的值本身,而引用型變數所引用的物件仍然儲存在堆空間中。

例如,Java 虛擬機器執行如清單6-1所示的程式所涉及的執行時空間如圖6-1所示(圖中箭頭表示引用型變數對相應物件的引用關係).