1.線程安全:當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方法進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那這個對象就是線程安全的。 2.Java語言中的線程安全 根據線程安全的安全程度由強到弱來排序,我們可以把 ...
1.線程安全:當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方法進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那這個對象就是線程安全的。
2.Java語言中的線程安全
根據線程安全的安全程度由強到弱來排序,我們可以把Java語言中各種操作共用的數據分為以下5類:不可變、絕對線程安全、相對線程安全、線程相容和線程對立。
1)不可變:不可變的對象一定是線程安全的;Java語言中,如果共用數據是一個基本數據類型,那麼只要在定義時使用final關鍵字修飾它就可以保證它是不可變的。如果共用數據是一個對象,那就需要保證對象的行為不會對其狀態產生任何影響,即把對象中帶有狀態的變數都聲明為final,這樣在構造函數結束之後,它就是不可變的。
2)絕對線程安全:就是一個類在任何運行時環境下,調用者都不需要任何額外的同步措施。在Java API中標註自己是線程安全的類,大多數都不是絕對線程安全的。
3)相對線程安全:就是我們通常所講的線程安全,它需要保證對這個對象單獨的操作是線程安全的,我們在調用時不需要做額外的保障措施。但對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。
4)線程相容:指對象本身並不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在併發環境下可以安全地使用。
5)線程對立:指無論調用端是否採取同步措施,都無法在多線程環境中併發使用的代碼。
3.線程安全的實現方法
1)互斥同步:同步是指在多個線程併發訪問共用數據時,保證共用數據在同一時刻只被一個線程使用,而互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式。
在Java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字經過編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令,這兩個位元組碼指令都需要一個reference類型的參數來指明鎖定和解鎖的對象。如果Java程式中的synchronized明確指定了對象參數,那就是這個對象的reference;如果沒有明確指定,那就是synchronized修飾的是實例方法還是類方法,去取對應的對象實例或class對象作為鎖對象。
根據虛擬機規範的要求,在執行monitorenter指令時,首先要嘗試獲取對象鎖。如果這個對象沒有被鎖定,或當前線程已經擁有了那個對象的鎖,把鎖的計數器加1;在執行monitorexit指令時會將鎖計數器減1,當計數器為0時,鎖就被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放為止。
synchronized同步塊對同一個線程來說是可重入的,不會出現把自己鎖死的問題。其次,同步塊在已經進入的線程執行完成之前,會阻塞後面其他線程的進入。
除了synchronized之外,我們還可以使用java.util.concurrent包中的重入鎖(ReentrantLock)來實現同步。他們都具備一樣的線程重入特性,一個表現為API層的互斥鎖(lock和unlock方法配合try/finally語句塊來完成),另一個表現為原生語法層面的互斥鎖。不過相比synchronized,ReentrantLock增加了一些高級功能,主要有:等待可中斷、可實現公平鎖以及鎖可以綁定多個條件。
等待可中斷是指當持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待,改為處理其他事情。
公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。而非公平鎖是在鎖被釋放時,任何一個等待鎖是線程都有機會獲得鎖。synchronized中的鎖就是非公平的,ReentrantLock預設情況下也是非公平的,但可以通過帶有布爾值的構造函數要求使用公平鎖。
鎖綁定多個條件是指一個ReentrantLock對象可以同時綁定多個Condition對象,只需要多次調用newCondition()方法即可。
2)非阻塞同步:基於衝突檢測的樂觀併發策略,就是先進行操作,如果沒有其他線程爭用共用資源,那操作就成功了;如果共用數據有爭用,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷重試,直到成功為止),這種樂觀的併發策略的許多實現都不需要把線程掛起,因此這種同步操作稱為非阻塞同步。
CAS指令需要有3個參數,分別為記憶體知(V)、舊的預期值(A)、新值(B)。當且僅當V符合舊預期值A時,處理器用新值B更新V值,否則就不執行更新,但是無論是否更新了V值,都會返回V的舊值。上述過程是一個原子操作。CAS存在的問題:如果一個變數V初次讀取時是A值,並且在準備賦值時檢查它仍為A值,那我們就說它的值沒有被其他線程變過了嗎?如果在這段期間曾被改成B,後來又被改回A,那CAS操作就誤以為它沒有被改變過,這個漏洞稱為CAS的ABA問題。JUC包為瞭解決此問題,提供了一個帶有標記的原子引用類,它可以通過控制變數值的版本來保證CAS的正確性。
3)無同步方案:如果一個方法就不涉及共用數據,那就自然無須任何同步措施去保證同步。
可重入代碼:可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼,而在控制權返回後,原來的程式不會出現任何錯誤。所有可重入的代碼都是線程安全的。判斷代碼是否具備可重入性:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的數據,就能返回相同的結果,那它就滿足可重入性的要求,也就是線程安全的。
線程本地存儲:如果一個變數要被某個線程共用,可以通過java.util.ThreadLocal類來實現線程本地存儲的功能。
4.鎖優化
1)自旋鎖與自適應自旋:讓後面請求鎖的那個線程等待一會,但不放棄處理器的執行時間,看看持有鎖的線程是否很快會釋放鎖。為了讓線程等待,我們可以讓線程執行一個忙迴圈(自旋),這項技術就是自旋鎖。如果鎖被占用的時間很短,自旋等待的效果就非常好;反之,鎖占用的時間很長,那麼自旋線程只會白白浪費處理器資源,因此,如果自旋超過了限定的次數仍然沒有獲得鎖,就應將線程掛起。自旋次數預設值為10次。
JDK1.6之後引入了自適應的自旋鎖,意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的。
2)鎖消除:是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共用數據競爭的鎖進行消除。鎖消除的主要判定依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當作棧上數據對待,認為它們是線程私有的,同步加鎖自然就無須進行。
3)鎖粗化:如果虛擬機探測到一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部。
4)輕量級鎖:本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。
輕量級鎖的執行過程:在代碼進入同步塊時,如果此同步對象沒有被鎖定(鎖標誌位為“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的運行時數據的拷貝。然後,虛擬機將使用CAS操作嘗試將對象的運行時數據更新為指向鎖記錄的指針。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象運行時數據的鎖標誌位將轉變為“00”,即表示此對象處於輕量級鎖定狀態。如果這個更新操作失敗了,虛擬機首先會檢查對象的運行時數據是否指向當前線程的棧幀,如果只說明當前線程已經擁有了這個對象的鎖,那就直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶占了。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標誌位的狀態值為“10”,運行時數據中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。
解鎖過程:也是通過CAS操作來進行的,如果對象的運行時數據仍然指向著線程的鎖記錄,那就用CAS操作把對象當前的運行時數據的拷貝替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試獲得該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。
5)偏向鎖:偏向鎖就是在無競爭的情況下把整個同步都消除掉。偏向鎖的偏意思是這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
假設當虛擬機啟用了偏向鎖,那麼,當鎖對象第一次被線程獲取時,虛擬機將會把對象頭中的標誌位設為“01”,即偏向模式。同時使用CAS操作把獲取到的這個鎖的線程ID記錄在對象的運行時數據中,如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作。當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。