1. 程式人生 > >jvm筆記07:執行緒安全與鎖優化

jvm筆記07:執行緒安全與鎖優化

 java語言中的執行緒安全

        按照執行緒安全的”安全程度”由強至弱來排序,我們可以將java語言中各種操作共享的資料分為以下5類:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容和執行緒對立

不可變
         不可變的的物件一定是執行緒安全的。無論是物件的方法實現還是方法的呼叫者,都不需要再採用任何的執行緒安全保障措施 。java語言中,如果共享資料是一個基本資料型別那麼只要在定義時候使用final關鍵字修飾它就可以保證它是不可變的。如果共享資料是一個物件,那就需要保證物件的行為不會對其狀態產生任何影響,保證物件行為不影響自己狀態的途徑有很多種,其中最簡單的就是把物件中帶有狀態的變數都宣告為final,這樣的話,在建構函式結束之後,他就是不可變的。
絕對執行緒安全
         在Java API 中標註自己是執行緒安全的類,大多數都不是絕對的執行緒安全。例如Vector,雖然他的add,get,size都被synchronized修飾了,但是不代表呼叫它的時候永遠都不需要同步手段了。最簡單的例子就是,新建一個擁有20個元素的Vector,開一個執行緒去移除Vector,開一個執行緒去列印Vector,會發現陣列越界的錯誤訊息。
相對執行緒安全
         執行緒的相對安全就是我們平常所說的執行緒安全。他需要保證這個物件單獨的操作時執行緒安全的,我們在呼叫的時候不需要做額外的保障措施,但是對於一些特定順序的連續呼叫,就可能需要在呼叫的端使用額外的同步手段來保證呼叫的正確性。        在java語言中,大部分的執行緒安全類都是屬於這種型別,例如Vector,HashTable Collections的synchronizedCollection()方法包裝的集合等。
執行緒相容
          執行緒相容是指物件本身不是執行緒安全的,但是可以通過在呼叫端正確的使用同步手段來保證物件在併發的環境中可以安全的使用。如ArrayList和HashMap
執行緒對立
         執行緒對立是指無論呼叫端是否採用了同步的措施,都無法在多執行緒環境中併發使用的程式碼。例如Thread的 suspend和resume方法。

執行緒安全實現方法

互斥同步

      互斥同步(Mutual Exception & Synchronized) 是常見的一種併發正確性保障手段。

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

       在虛擬機器規範對monitorenter 和monitorexit的行為描述中,有兩點是需要特別注意的。首先synchronized同步塊對同一條執行緒來說是可重入的,不會出現自己把自己鎖死的問題。其次,同步塊在已經進入執行緒執行完之前,會阻塞後面其他執行緒的進入。如果要阻塞一個執行緒或者喚醒一個執行緒都需要作業系統來幫忙完成,這就需要從使用者態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。對於簡單的程式碼塊,狀態轉換消耗的時間可能比使用者程式碼的執行時間還要長。所以synchronized是Java語言中一個重量級的操作。不過虛擬機器本身也會進行一些優化,譬如在通知作業系統阻塞執行緒之前加入一段自旋等待,避免頻繁的切入到核心態之中。

        除了synchronized之外,我們還可以使用java.util.concurrent 包中的重入鎖(ReentrantLock)來實現同步,相對synchronized來說,ReentrantLock增加了一些高階功能,主要有以下三項: 等待可中斷、可實現公平鎖、鎖可以繫結多個條件。

1)等待中斷是指 當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,改為處理其他事情。

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

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

       後續的JDK1.6釋出之後人們發現synchronized和ReentrantLock的效能基本持平,由於synchronized 是原生的,所以在能實現需求的情況下,還是提倡使用synchronized。

非阻塞同步        
       互斥同步最主要的問題就是進行執行緒阻塞和喚醒所帶來的效能問題,因此這種同步也成為阻塞同步。所以處理問題的方式上說,互斥同步屬於悲觀鎖的併發策略,總是認為不去做正確的同步措施,那就肯定會出錯,無論共享資料還是真的會出現競爭,它都要進行加鎖。       隨著硬體指令集的發展,基於衝突檢測的樂觀鎖併發策略出現了,通俗的說,就是先進行操作,如果沒有其他執行緒競爭用共享資料,那操作就成功了;如果共享資料有競爭,產生了衝突,那就在採取 其他的補償措施(最常見的補償措施就是不斷嘗試,知道成功為止),這種樂觀鎖的併發策略的許多實現都不需要把執行緒掛起,因此這種操作同步稱作非阻塞同步。       樂觀鎖,是基於CAS原理實現的,但是CAS也有邏輯缺陷,就是會有可能引發"ABA" 問題,不過大部分情況下,ABA問題不會影響程式併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能迴避原子類更高效。
無同步方案
       執行緒安全不一定需要進行同步,兩者沒有因果關係,同步只是保證資料爭用時候的正確性。如果一個方法本來就不涉及共享資料,那他自然就無須任何同步操作,因此會有一些程式碼天生就是執行緒安全的. 可重入程式碼(Reentant Code):可重入程式碼有一些共性,例如不依賴儲存在堆上的資料,和共用的系統資源,用到的狀態量都是引數傳入、不掉用非可重入的方法等。如果一個方法,只要輸入了相同的引數,就能返回相同的結果,那他就滿足可重入性的要求,也就是執行緒安全的。 執行緒本地儲存(Thread Local Storage):如果一段程式碼所需要的資料必須與其他程式碼共享,那就看看這些共享資料的程式碼是否能保證在同一個執行緒執行?如果能保證,我們就可把共享資料的可見範圍限制在同一個執行緒之內,這樣,無需同步也能保證執行緒之間不出現資料爭用的問題。          java語言中如果一個變數要被多個執行緒訪問,可以使用volatile關鍵字宣告它為“易變得”;如果一個變數要被某個執行緒獨享,可以通過ThreadLocal 類來實現執行緒本地儲存的功能。每一個執行緒物件中都有一個ThreadLocalMap物件,這個物件儲存了一組以ThreadLocal.threadLocalHashCode為鍵,以本地執行緒變數為值得K-V值對,ThreadLocal物件就是當前執行緒的ThreadLocalMap的訪問入口,每一個ThreadLocal物件都包含了一個獨一無二的threadLocalHashMap值,使用這個值就可以線上程K-V 值對中找回對應的本地執行緒變數。

鎖優化

自旋鎖與自適應自旋

          由於同步互斥中國,對效能影響最大的是阻塞的實現,掛起執行緒金和恢復執行緒的操作都需要轉入核心態完成,這些操作給系統的併發效能帶來壓力,同時虛擬機器開發團隊注意到共享資料的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復執行緒並不值得,如果物理機器有兩個以上的處理器,能讓兩個或者兩個以上的執行緒同時執行,我們就可以讓後面請求鎖的那個執行緒 “稍等一下”但不放棄處理器執行時間,看看鎖是否很快釋放。為了讓執行緒等待,我們只需要讓執行緒執行一個忙迴圈(自旋),這項技術就是所謂的自旋鎖。

         自旋如果超過限定次數還沒有獲得鎖的話,那麼就會掛起,限定次數可以通過-XX:PreBlockSpin 來修改,預設是10。 

        JDK1.6以後預設開啟自旋鎖,而且引入了自適應自旋鎖。自適應意味著自旋的時間不在固定,而是由前一次在同一個鎖上的自旋時間以及所的擁有者狀態來決定。如果上一次自旋獲取鎖成功過,那麼虛擬機器會認為這次自旋也很有可能成功,進而允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功,那麼以後獲取鎖的時候會省略掉自旋的過程,避免浪費CPU資源(自旋是需要浪費CPU資源的)。

鎖消除

       鎖消除,是指虛擬機器即時編譯在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除。

鎖粗化

       原則上,我們再編寫程式碼的時候,總是推薦將同步塊的作用範圍限制在儘量小,只在共享資料的實際作用域才進行同步,這樣是為了使得需要同步的運算元量儘可能變小,如果存在鎖競爭,那等待鎖的執行緒也能儘快拿到鎖。

      大部分情況下,上面的原則是正確的,但是如果一些列的連續操作都對同一個物件反覆加鎖和解鎖,甚至加鎖操作時出現在迴圈體重的,那即使沒有執行緒競爭,頻繁的進行互斥同步操作也會導致不必要的效能消耗。所以這個時候可以把鎖粗化,將鎖提到迴圈的外面。

輕量級鎖

        輕量級鎖是JDK1.6中加入的新型鎖機制,他們名字中的“輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就稱為“重量級”鎖。需要強調一點的就是輕量級鎖不是來替代重量級鎖的,他的本意是在沒有多執行緒的競爭條件下,減少傳統的重量級鎖 使用 以減少作業系統互斥量產生的效能消耗。

       輕量級鎖提升程式同步效能的依據是“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗資料,如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但是如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭下,輕量級鎖迴避傳統的重量級鎖慢。

偏向鎖

      偏向鎖也是JDK1.6引入的,它的目的是消除資料在無競爭狀態下的同步原語,進一步提高程式執行效能。也就是說輕量級鎖是在無競爭條件下使用CAS操作去消除同步使用的互斥量,那麼偏向鎖就是在無競爭情況下把整個同步都消除掉,連CAS操作都不做了。

    偏向鎖可以提高帶有同步但無競爭的程式效能。