1. 程式人生 > >深入理解java虛擬機器----第十三章執行緒安全與鎖優化

深入理解java虛擬機器----第十三章執行緒安全與鎖優化

這一部分和java併發程式設計實戰中講的很多東西一樣,所以可以對照著看。

13.1 概述

對於這部分的主題“高效併發”來講,首先需要保證併發的正確性,然後在此基礎上實現高效。本章先從如何保證併發的正確性和如何實現執行緒安全講起。

13.2 執行緒安全

當多個執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方法進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那麼這個物件是執行緒安全的。

13.2.1 java語音中的執行緒安全

這裡討論的執行緒安全,就限定於多個執行緒之間存在共享資料訪問的這個前提。

按照執行緒安全的“安全程度”由強到弱排序,可以把Java中各個操作共享的資料分為以下5類:

1 不可變

不可變(Immutable)的物件一定是執行緒安全的。

如果共享資料是一個基本資料型別,定義時使用final關鍵字修飾可保證它不可變。

如果共享資料是一個物件,那就需要保證物件的行為不會對其狀態產生任何影響才行。其中最簡單的是把物件中帶有狀態的變數都宣告為final,這樣在建構函式結束後,它就是不可變的。

Java API中符合不可變要求的型別:java.lang.String/java.lang.Number部分子類等。

2 絕對執行緒安全

一個類要想達到絕對執行緒安全,需要做到無論什麼時候,都不需要額外的同步措施。這需要很大很大的代價。大多Java API標記的執行緒安全類,都不是絕對執行緒安全。比如Vector,它的get、add、size等方法都是被synchronized修飾的,單獨呼叫時是安全的,但以一些特殊順序呼叫一系列的方法時,就不安全了。

3 相對執行緒安全

對一個物件的一個單獨操作是執行緒安全的。例如Vector、HashTable等。

4 執行緒相容

本身不安全,通過同步手段可以達到安全的目的。Java API中大多數都是這類的。

5 執行緒對立

無論是否同步,都不能在多執行緒中併發使用。suspend和resume等。

13.2.2 執行緒安全的實現方法

1 互斥同步

互斥同步(Mutual Exclusion & Synchronization)是常見的一種併發正確性保障手段。同步是指在多個執行緒併發訪問共享資料時,保證共享資料在同一個時刻只被一個(或者是一些,使用訊號量的時候)執行緒使用。而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(MuTex)和訊號量(Semaphore)都是主要的互斥實現方式。因此,在這四個字裡面,互斥是因,同步是果;互斥是方法,同步是目的。

在Java中,最基本的互斥同步手段是synchronized關鍵字,synchronized關鍵字經過編譯後,會在同步程式碼塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令,這兩個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件。如果程式中synchronized指明瞭物件引數,那就是這個物件的reference;如果沒有指明,那就根據synchronized修飾的是例項方法還是類方法,去取對應的物件例項或Class物件來作為鎖物件。

虛擬機器規範要求,在執行monitorenter指令時,首先嚐試獲取物件的鎖。如果物件沒有被鎖定或者當前執行緒已經擁有了那麼物件的鎖,把鎖的計數器加1,執行monitorexit時,將鎖計數減1,當鎖計數器為0時,鎖被釋放。如果獲取物件鎖失敗,當前執行緒將阻塞等待。

虛擬機器對monitorenter和monitorexit行為描述中,注意兩點:synchronized同步塊對同一條執行緒來說是可重入的,不會出現自己把自己鎖死的問題;同步塊在已進入執行緒執行完之前,會阻塞後面其他執行緒的進入。

還可以使用java.util.concurrent(以下稱JUC)包中的重入鎖(ReentrantLock)來實現同步。相比synchronized,ReentrantLock增加了一些高階功能,主要以下3項:等待可中斷、可實現公平鎖,以及鎖可以繫結多個條件。

等待可中斷:當持有的鎖的執行緒長期不釋放鎖時,正在等待的執行緒可以選擇放棄等待,改為處理其他事情,對處理執行時間長的同步塊很有幫助。

公平鎖:多個執行緒等待同一鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖不保證這一點,在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock預設也是非公平,但可以通過建構函式要求使用公平鎖。

繫結多個條件:一個ReentrantLock物件可以同時繫結多個Condition物件,而synchronized中,鎖物件的wait()和notify()或notifyAll()可以實現一個隱含的條件,如果要和多於一個的條件關聯時,就不得不額外新增一個鎖,而ReentrantLock則無須這樣,只要多次呼叫newCondition()即可。

 

2.非阻塞同步

互斥同步最主要的問題就是進行執行緒阻塞和喚醒所帶來的效能問題,因此這種同步也稱為阻塞同步(Blocking Synchronized)。處理問題方式上,互斥同步屬於一種悲觀的併發策略,總是認為只要不去做正確的同步措施(例如加鎖),那肯定會出現問題,無論共享資料是否真的出現競爭,它都要進行加鎖(這裡討論的是概念模型,實際上虛擬機器會優化很大一部分不必要的加鎖)、使用者態核心態轉換、維護鎖計數器和檢查是否有阻塞的執行緒需要等待喚醒等操作。

隨著硬體指令集的發展,有了另外一種選擇:基於衝突檢測的樂觀併發策略,就是先進性操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享資料有爭,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重試,直到成功為止),這種樂觀的併發策略的許多實現都不需要把執行緒掛起,因此這種同步稱為非阻塞同步(Non-Blocking Synchronization)。

我們需要操作和衝突檢測這兩個步驟具備原子性,只能靠硬體來完成,硬體保證一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成,這類執行常用的有:

測試並設定(Test and Set)。

獲取並增加(Fetch and Increment)。

交換(Swap)。

比較並交換(Compare and Swap,以下稱CAS)。

載入連結/條件儲存(Load Linked/Store Conditional,以下稱LL/SC)。

3.無同步方案

要保證執行緒安全,並不是一定就要進行同步,兩者並沒有因果關係。同步只是保證共享資料爭用時正確性的手段,如果一個方法本來就不涉及共享資料,自然就無須任何同步措施去保證正確定。因此會有一些程式碼天生就是執行緒安全的。兩類:

可重入程式碼(Reentrant Code):這種程式碼也叫純程式碼(Pure Code),可以在程式碼執行的任何時刻中斷它,轉而去執行另外一斷程式碼(包括遞迴呼叫它本身),而在控制權返回後,原來的程式不會出現任何錯誤。所有可重入程式碼都是執行緒安全的。可重入程式碼有一些共同特徵,例如不依賴儲存在堆上的資料和公用的系統資源、用到的狀態量都是由引數中傳入、不呼叫非可重入的方法等。判斷程式碼具備可重入的簡單原則:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的資料,就都能返回相同的結果,就滿足可重入性的要求,當然也是執行緒安全的。

執行緒本地儲存(Thread Local Storage):如果一段程式碼中所需要的資料必須與其他程式碼共享,那就看這些共享資料的程式碼是否能保證在同一個執行緒中執行?如能,就把共享資料的可見性範圍限制在同一個執行緒之內,這樣,就無須同步也能保證執行緒之前不出現資料爭用問題。

符合這種特點的應用:大部分使用消費佇列的架構模式(如“生產者-消費者”模式)都會講消費過程儘量在一個執行緒中消費完;經典Web互動模型中的“一個請求對應一個服務執行緒”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多Web服務端應用都可以使用執行緒本地儲存來解決執行緒安全問題。

Java中,如果一個變數要被多個執行緒訪問,可以使用volatile關鍵字宣告它為“易變的”;如果一個變數被某個執行緒獨享,可以通過java.lang.ThreadLocal類來實現執行緒本地儲存的功能。每一個執行緒的Thread物件中都有一個ThreadLcoalMap物件,該物件儲存了一組易ThreadLocal.threadLocalHashCode為鍵,以本地執行緒變數為值得K-V鍵值對,ThreadLocal物件就是當前執行緒的ThreadLocalMap的訪問入口,每一個ThreadLocal物件都包含了一個獨一無二的threadLocalHashCode值,使用這個值就可以線上程K-V鍵值對中找回對應的本地執行緒變數。

13.3 鎖優化

自旋鎖:執行緒的掛起和恢復代價很大,自旋鎖讓競爭鎖的執行緒自迴圈等待一會,看不能不能很快獲得鎖。省去了執行緒切換代價,但是自迴圈白白消耗cpu,所以它會自適應,收集資料判斷等待是否值得,預設迴圈10次,如果預測值得就會加大次數,如果預測不值得,可能會直接掛起。

鎖消除:利用逃逸分析的結果,去掉不必要的鎖

鎖粗化:如果小範圍內頻繁對一個物件加鎖、釋放鎖,開銷很大,會將鎖範圍擴大,用更少量的鎖代替,減少開銷。

輕量級鎖:傳統鎖使用互斥量實現,開銷大。它的目的是 在沒有多執行緒競爭的前提下,減少傳統鎖的開銷。前面的物件記憶體佈局中提到過,HotSpot物件頭會儲存執行時資訊Mark Word,其中就包括鎖資訊。如果執行緒發現物件當前沒有被鎖定,將會在棧楨建立一個名為鎖記錄的空間,儲存當前鎖物件的Mark Word拷貝,然後用CAS嘗試將物件頭中的這部分改為指向該鎖記錄的指標。如果成功了,就獲得了鎖,並將Mark Word標記為輕量級鎖定狀態。如果失敗了,看佔有鎖的是不是這個執行緒本身,是的話,可以繼續執行,不是的話,說明產生競爭了,輕量級鎖將會膨脹為重量級鎖。釋放鎖同樣CAS。可以看出,如果沒有多執行緒競爭,大大提升效能,但是如果競爭出現,在傳統鎖基礎上又繞遠了一圈。

偏向鎖:目的是消除無競爭下的鎖原語,進一步提升效能。輕量級鎖是用CAS代理互斥量,偏向所是全去掉都不要了。鎖偏向於獲得它的執行緒,會在Mark Work中儲存獲得它的執行緒id,每次發現還是這個執行緒id就什麼都不需要做,直接放行。如果產生競爭,可能重偏向或膨脹為輕量級鎖。