理解JVM(六):線程安全和鎖優化

線程安全的實現方法

互斥同步

互斥是因,同步是果;互斥是方法,同步是目的。java

synchronized關鍵字

  • synchronized關鍵字是基本的互斥同步手段。它在編譯後會在同步代碼塊先後加入2條字節碼指令:monitorentermonitorexit
  • 這兩個字節碼都須要一個reference類型的參數來指明要鎖定和解鎖的對象。若是Java程序中的synchronized指定了對象參數,那就是這個對象的reference;若是沒有指定,就根據synchronized修飾的是實例方法仍是類方法,去取對應的對象實例或Class對象來做爲鎖對象。
  • 執行monitorenter指令時,首先要嘗試獲取對象的鎖。若是這個對象沒被鎖定,或當前線程已經擁有了那個對象的鎖,把鎖的計數器加1;在執行monitorexit指令時會將鎖計數器減1。當計數器爲0時,鎖就被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放爲止。
  • synchronized同步塊對同一條線程來講是可重入的,不會出現本身把本身鎖死的問題。
  • 同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入。
  • Java的線程是映射到操做系統的原生線程之上的,若是要阻塞或喚醒一個線程,都須要操做系統來完成,這就須要從用戶態轉換到核心態中,所以狀態轉換須要耗費不少的處理器時間,因此synchronized是Java語言中一個重量級的操做。不過虛擬機會有一些優化措施,好比自旋等待。

ReentrantLock重入鎖

重入鎖位於java.util.concurrent包。基本用法和synchronized類似,只是代碼寫法有區別:synchronized是原生語法層面的實現。ReentrantLock是API層面,使用lock()unlock()方法配合try/finally語句塊來實現。安全

重入鎖有3個高級特性:多線程

  • 等待可中斷:當持有鎖的線程長期不釋放鎖時,正在等待的線程能夠選擇放棄等待,改成處理其餘事情。可中斷特性對處理執行時間很是長的同步塊頗有幫助。
  • 可實現公平鎖:公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖。synchronized中的鎖是非公平的,ReentrantLock默認狀況下也是非公平的,但能夠經過帶布爾值的構造函數要求使用公平鎖。
  • 鎖能夠綁定多個條件:一個ReentrantLock對象能夠同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()notify()notifyAll()方法能夠實現一個隱含的條件,若是要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這樣作,只須要屢次調用newCondition()方法便可。

性能比較

  • JDK1.6以前,在多線程環境下,synchronized的吞吐量隨着處理器數量增長而降低得很是嚴重。
  • JDK1.6以後,虛擬機作了優化,2種方式性能差很少。推薦優先使用synchronized方式。

非阻塞同步

互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,所以這種同步也稱爲阻塞同步(Blocking Synchronization)。併發

按處理問題的方式來講:函數

  • 互斥同步是悲觀併發策略:不管是否產生共享數據爭用,都會作同步措施(加鎖,用戶態內核態轉換等)。
  • 非阻塞同步是一種樂觀併發策略:它基於衝突檢測。通俗的說,就是先執行代碼,若沒有發生共享數據爭用,就成功執行;若發生共享數據爭用,就採起補償措施(好比不斷重試,直到成功),這種策略不會致使線程阻塞。

CAS操做: CAS指令須要有3個操做數,分別是內存位置(在Java中能夠簡單理解爲變量的內存地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,不然它就不執行更新,可是不管是否更新了V的值,都會返回V的舊值,這個處理過程是個原子操做。性能

ABA問題: 若是一個變量V初次讀取的時候是A值,而且在準備賦值的時候檢查到它仍然爲A值,那咱們就能說它的值沒有被其餘線程改變過了嗎?若是在這段期間它的值曾經被改爲了B,後來又被改回爲A,那CAS操做就會誤認爲它歷來沒有被改變過。優化

無同步方案

若是一個方法原本就不涉及共享數據,那它就無須任何同步措施去保證正確性。操作系統

  • 可重入代碼:這種代碼也叫作純代碼(Pure Code),能夠在代碼執行的任什麼時候刻中斷它,轉而去執行另一段代碼(包括遞歸調用它自己),而在控制權返回後,原來的程序不會出現任何錯誤。
  • 線程本地存儲:一段代碼中所須要的數據必須與其餘代碼共享,而且能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。
    • Java語言中,若是一個變量要被多線程訪問,可使用volatile關鍵字聲明它爲「易變的」;若是一個變量要被某個線程獨享,Java中就沒有相似C++中__declspec(thread) 這樣的關鍵字,不過仍是能夠經過java.lang.ThreadLocal類來實現線程本地存儲的功能。每個線程的Thread對象中都有一個ThreadLocalMap對象,這個對象存儲了一組以ThreadLocal.threadLocalHashCode爲鍵,以本地線程變量爲值的K-V值對,ThreadLocal對象就是當前線程的ThreadLocalMap的訪問入口,每個ThreadLocal對象都包含了一個獨一無二的threadLocalHashCode值,使用這個值就能夠在線程K-V值對中找回對應的本地線程變量。

鎖優化

適應性自旋(Adaptive Spinning)

線程阻塞的時候,讓等待的線程不放棄cpu執行時間,而是執行一個自旋(通常是空循環),這叫作自旋鎖。線程

自旋等待自己雖然避免了線程切換的開銷,但它是要佔用處理器時間的,所以,若是鎖被佔用的時間很短,自旋等待的效果就很是好,反之,若是鎖被佔用的時間很長,那麼自旋的線程只會白白消耗處理器資源,帶來性能上的浪費。指針

所以,自旋等待的時間必需要有必定的限度。若是自旋超過了限定的次數仍然沒有成功得到鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可使用參數-XX:PreBlockSpin來更改。

JDK1.6引入了自適應的自旋鎖。自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。好比前一次自旋了3次就得到了一個鎖,那麼下一次虛擬機會容許他自旋更屢次來得到這個鎖。若是一個鎖不多能經過自旋成功得到,那麼以後再遇到這個狀況就會省略自旋過程了。

鎖消除(Lock Elimination)

虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。通常根據逃逸分析的數據支持來做爲斷定依據。

鎖粗化(Lock Coarsening)

原則上,咱們在編寫代碼的時候,老是推薦將同步塊的做用範圍限制得儘可能小——只在共享數據的實際做用域中才進行同步,這樣是爲了使須要同步的操做數量儘量變小,若是存在鎖競爭,那等待鎖的線程也能儘快拿到鎖。

但若是一系列操做頻繁對同一個對象加鎖解鎖,或者加鎖操做再循環體內,會耗費性能,這時虛擬機會擴大加鎖範圍。

輕量級鎖(Lightweight Locking)

輕量級鎖是JDK 1.6之中加入的新型鎖機制。它的做用是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。

HotSpot虛擬機的對象頭(Object Header)分爲兩部分信息,第一部分用於存儲對象自身的運行時數據,這部分稱爲Mark Word。還有一部分存儲指向方法區對象類型數據的指針。

加鎖

在代碼進入同步塊的時候,若是此同步對象沒有被鎖定(鎖標誌位爲「01」狀態),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word)。而後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針。若是這個更新動做成功,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變爲「00」,即表示此對象處於輕量級鎖定狀態。若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其餘線程搶佔了。若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

解鎖

解鎖過程也是經過CAS操做來進行的。若是對象的Mark Word仍然指向着線程的鎖記錄,那就用CAS操做把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,若是替換成功,整個同步過程就完成了。若是替換失敗,說明有其餘線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。

性能

沒有鎖競爭時,輕量級鎖用CAS操做替代互斥量的開銷,性能較優。有鎖競爭時,除了互斥量開銷,還有CAS操做開銷,因此性能較差。可是,通常狀況下,在整個同步週期內都是不存在競爭的」,這是一個經驗數據。

偏向鎖(Biased Locking)

偏向鎖也是JDK1.6中引入的鎖優化,它的目的是消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。若是說輕量級鎖是在無競爭的狀況下使用CAS操做去消除同步使用的互斥量,那偏向鎖就是在無競爭的狀況下把整個同步都消除掉,連CAS操做都不作了。

當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲「01」,即偏向模式。同時使用CAS操做把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做。當有另一個線程去嘗試獲取這個鎖時,偏向模式結束。

偏向鎖能夠提升帶有同步但無競爭的程序性能,但並不必定老是對程序運行有利。若是程序中大多數的鎖老是被多個不一樣的線程訪問,那偏向模式就是多餘的。在具體問題具體分析的前提下,有時候使用參數-XX:-UseBiasedLocking來禁止偏向鎖優化反而能夠提高性能。

相關文章
相關標籤/搜索