JVM執行緒安全與鎖優化
執行緒安全
《Java Concurrency In Practice》的作者Brian Goetz對執行緒安全的定義:當多個執行緒訪問一個物件時,如果不用老驢這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那這個物件是執行緒安全的。
Java語言中的執行緒安全
按照執行緒安全的“安全程度”由強至弱排序,可以將Java語言中各種操作共享的資料分為5類:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容和執行緒對立。
-
不可變
在Java語言中,不可變的物件一定是執行緒安全的,無論是物件的方法實現還是方法的呼叫者,都不需要再採取任何的執行緒安全保障措施。如果共享資料是一個基本資料型別,那麼只要在定義時使用final關鍵字修飾就可以保證是不可變的。如果共享的是一個物件,那就需要保證物件的行為不會對其狀態產生任何影響才行,例如java.lang.String類。
-
絕對執行緒安全
絕對的執行緒安全完全滿足Brian Goetz給出的執行緒安全的定義,一個類要達到“不管執行時環境如何,呼叫者都不需要任何額外的同步措施”通常需要付出很大的。甚至是不切實際的代價。
-
相對執行緒安全
相對執行緒安全就是我們通常意義上所講的執行緒安全,它需要保證對這個物件單獨的操作時執行緒安全的,我們在呼叫的時候不需要額外的保障措施,但是對於一些特定順序的連續呼叫,就可能需要我們呼叫端使用額外的同步手段來保證呼叫的正確性。
-
執行緒相容
執行緒相容是指物件本身並不是執行緒安全的,但是可以通過在呼叫端正確地使用同步手段來保證物件在併發環境中可以安全地使用。例如:ArrayList類和HashMap類。
-
執行緒對立
執行緒對立是指無論呼叫端是否採取了同步措施,都無法在多執行緒環境中併發使用的程式碼。例如:Thread類的suspend()方法和resume()方法,如果有兩個執行緒同時持有一個執行緒物件,一個嘗試中斷執行緒,另一個嘗試恢復執行緒,如果併發執行的話,無論呼叫時是否進行了同步,目標執行緒都存在死鎖的風險。
執行緒安全的實現方法
-
互斥同步
互斥同步是常見的一種併發正確保障手段。同步是指在多個執行緒併發訪問共享資料時,保證共享資料在同一個時刻只被一個(或者是一些,使用訊號量的時候)執行緒使用。而互斥是實現同步的一種手段,臨界區、互斥量和訊號量都是主要的互斥實現方式。
在Java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字經過編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令,這兩個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件。在虛擬機器規範對monitorenter和monitorexit的行為描述中,synchronized同步塊對同一條執行緒來說是可重入的,不會出現自己把自己鎖死的問題。同步塊在已進入的執行緒執行完之前,會阻塞後面其他執行緒的進入。
除了synchronized之外,還可以使用java.util.concurrent包中的重入鎖(ReentrantLock)來實現同步。相比synchronized相比,ReentrantLock增加了一些高階功能,主要有3項:等待可中斷、可實現公平鎖、以及鎖可以繫結多個條件。
- 等待可中斷是指當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情,可中斷特性對處理執行時間非常長的同步塊有幫助。
- 公平鎖是指多個執行緒在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock預設情況下也是非公平的,但可以通過帶有布林值的建構函式要求使用公平鎖。
- 鎖繫結多個條件是指一個ReentrantLock物件可以同時繫結多個Condition物件,而在synchronized中,鎖物件的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多餘一個的條件關聯的時候,就不得不額外地新增一個鎖,而ReentrantLock則無需這樣做,只需要多次呼叫newCondition()方法即可。
-
非阻塞同步
互斥同步最主要地問題就是進行執行緒阻塞和喚醒所帶來地效能問題,因此這種同步也成為阻塞同步。隨著硬體指令集地發展,我們有了另外一種選擇,基於衝突檢測的樂觀併發策略,通俗的說,就是先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享資料有爭用,產生了衝突,那就再採取其他的補償措施,這種樂觀的併發策略的許多實現都不需要把執行緒掛起,因此這種同步操作稱為非阻塞同步。
CAS指令:
CAS指令需要有3個運算元,分別是記憶體位置(在Java中可以簡單理解為變數的記憶體地址、用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則它就不執行更新,但是無論是否更新了V的值,都會返回V的舊值,這個處理過程是一個原子操作。CAS存在的邏輯漏洞:如果一個變數V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然為A值,那我們就能說它的值沒有被其他執行緒改變過了嗎?如果在這段期間它的值曾經被改成了B,後來又被改回了A,那CAS操作就會誤認為它從來沒有改變過。這個漏洞被稱為CAS操作的“ABA”問題。
-
無同步方案
如果一個方法本來就不涉及共享資料,那它自然就無須任何同步措施去保證正確性,因此會有一些程式碼天生就是執行緒安全的。
-
可重入程式碼
如果一個方法,它的返回結果是可預測的,只要輸入了相同的資料,就都能返回相同的結果,那它就滿足可重入的要求,就是執行緒安全的。
-
執行緒本地儲存
如果一段程式碼中所需要的資料必須與其他程式碼共享,那就看看這些共享資料的程式碼是否能保證在同一個執行緒中執行?如果能保證,就可以把共享資料的可見範圍限制在同一個執行緒之內,這樣,無須同步也能保證執行緒之間不出現資料爭用的問題。
-
鎖優化
自旋鎖與自適應鎖
互斥同步對效能影響最大的是阻塞的實現,掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,這些操作給系統的併發效能帶來了很大的壓力。同時,虛擬機器的開發團隊也注意到在許多應用上,共享資料的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復執行緒並不值得。如果物理機器上有一個以上的處理器,能讓兩個或以上的執行緒同時並行執行,我們就可以讓後面的請求鎖的那個執行緒“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。為了讓執行緒等待,我們只需要讓執行緒執行一個忙迴圈,這項技術就是所謂的自旋鎖。
自適應自旋就是自旋的時間不再固定,而是由前一次在同一個鎖上自旋的時間及擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也很有可能再次成功,進而將它允許自旋等待持續相對更長的時間。另外,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲得這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。
鎖消除
鎖消除是指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除。
鎖粗化
原則上,在編寫程式碼的時候,總是推薦將同步塊的作用範圍限制得儘量小——只在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的運算元量儘可能變小,如果存在鎖競爭,那等待的執行緒也能儘快拿到鎖。大部分情況下,這個原則是正確的,但是如果一系列的連續操作都對同一個物件反覆加鎖和解鎖,甚至加鎖操作是出現在迴圈體中,那即使沒有執行緒競爭,頻繁地進行互斥同步操作也會導致不必要的效能損耗。如果虛擬機器探測到有這樣一串零碎的操作都對同一個物件加鎖,將會把鎖同步範圍擴充套件(粗化)到整個操作序列的外部。