JAVA鎖優化

鎖優化思路

最好的方式不加鎖,如果必須加鎖,可以從如下幾個方面入手進行鎖優化:

1. 減少鎖持有時間
2. 減小鎖粒度
3. 鎖分離
4. 鎖粗化

減少鎖的持有時間

減少鎖的持有時間,即減少鎖內代碼執行時間,可以通過減少鎖內代碼量實現,例如避免給整個方法加鎖、將不需要加鎖的代碼移出去,例如:

 public synchronized void doSomething() { 
     System.out.println("before");
     needLockCode(); 
     System.out.println("after");
 }
 
 改爲:
 
 public void doSomething() { 
     System.out.println("before");
     synchronized(this){ 
         needLockCode(); 
     } 
     System.out.println("after");
 }

或:

 public void doSomething() { 
     synchronized(this){ 
         System.out.println("before");
         needLockCode(); 
         System.out.println("after");
     } 
 }
 
 改爲:
 
  public void doSomething() { 
     System.out.println("before");
     synchronized(this){ 
         needLockCode(); 
     } 
     System.out.println("after");
 }

減小鎖的粒度

減小鎖的粒度,這個偏向於減小被鎖住代碼涉及的影響範圍的減小,降低鎖競爭的機率,例如jdk5的ConcurrentHashMap,ConcurrentHashMap不會爲整個hash表加鎖,而是將Hash表劃分爲多個分段,對每個段加鎖,這樣減小了鎖粒度,提升了併發處理效果。

再如假設有對象object,如果加鎖後,不允許對object操作,此時鎖粒度相當於object對象,如果實際上object只有一個名爲needLock字段可能會出現併發問題,此時將鎖加在這個字段上即可。

鎖分離

ReentrantLock和synchronized使用的是獨佔鎖,無論是讀或寫都保證同時只有一個線程執行被鎖代碼。但是單純的讀實際上不會引起併發問題。尤其是對於讀多寫少的場景,可以將讀和寫的鎖分離開來,可以有效提升系統的併發能力。

讀寫所在同一時刻可以允許多個線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程都會被阻塞。讀寫鎖維護了一對鎖:讀鎖和寫鎖。一般情況下,讀寫鎖的性能都會比排它鎖好,因爲大多數場景讀是多於寫的。

當執行讀操作的時候,需要獲取讀鎖,在併發訪問的時候,讀鎖不會被阻塞;在執行寫操作時線程必須要獲取寫鎖,當已經有線程持有寫鎖的情況下,所有的線程都會被阻塞。讀鎖和寫鎖關係:

讀鎖與讀鎖可以共享;
讀鎖與寫鎖互斥;
寫鎖與寫鎖互斥。

ReentrantReadWriteLock是提供了讀鎖和寫鎖:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    ...
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;

    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    ...
}

鎖粗化

必要的時候,將被鎖住的代碼量變多、鎖持有時間更長也是鎖優化的方式,但優化結果一定要使整體的執行效率變的更好,例如:

 for(int i = 0; i < 100; i++) {
     synchronized(lock) {
         needLockCode();             
     }
 }

 改爲:

 synchronized(lock) {
     for(int i = 0; i < 100; i++) {
         needLockCode();
     }
 }

改造後,儘管每個線程每次持有鎖的時間變長了,但減少了每個線程請求和釋放鎖的次數,而請求和釋放鎖也是要消耗資源的。

虛擬機的鎖優化

1、自旋鎖與自適應自旋

由於掛起線程和恢復線程都需要轉入內核態完成,給系統帶來很大壓力,同時,共享數據的鎖定狀態只會持續很短的一段時間,因此去掛起和恢復線程很不值得。因此,可以使線程執行一個自我循環,因爲對於執行時間短的代碼這一會可能就會釋放鎖,而線程就不需要進行一次阻塞與喚醒。

自旋等待不能代替阻塞,自旋本身雖然避免了線程切換的開銷,但是會佔用處理器時間,如果鎖被佔用時間短,自旋等待效果好;反之,自旋的線程只會白白浪費處理器資源;因此,要限制自旋等待時間,自旋次數默認值是10次,超過次數仍然沒有成功獲取鎖,就掛起線程,進入同步阻塞狀態。

自適應自旋更智能一些,它根據前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定自旋次數,如果對於某個鎖的自旋很少有成功獲得過鎖,就不自旋了,避免浪費CPU資源。如果自旋等待剛剛成功獲得過鎖,並且持有鎖的線程在運行,則認爲此次自旋很有可能成功,就允許自旋更多的次數。

2. 鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的目的主要是判定依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把他們當作棧數據對待,認爲它們是線程私有的,同步加鎖自然就無需進行。

有時候鎖是開發者無意中涉及到的,例如對於下面代碼:

    public static String getStr(String s1, String s2) {
        return s1 + s2;
    }

只進行了字符串的拼接,但其中的s1 + s2可能被虛擬機優化爲:

    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();

而append()涉及了synchronized:

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

append()中的鎖就是sb對象,如果該對象在方法中new的話,sb對象就不會逃逸到方法以外,jvm認爲此時不必要加鎖,此處的鎖就被安全的消除了。

3. 鎖粗化

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

但如果一系列操作頻繁對同一個對象加鎖解鎖,或者加鎖操作再循環體內,會耗費性能,這時虛擬機會擴大加鎖範圍來減少獲取鎖、釋放鎖的操作。具體可以看上文示例。

4. 輕量級鎖

輕量級鎖是JDK6之中加入的新型鎖機制,它名字中的“輕量級”是相對於使用操作系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就稱爲“重量級”鎖。首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

在代碼進入同步塊的時候,如果同步對象沒有被鎖定,虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄( Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象 Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變爲“00”,即表示此又對象處於輕量級鎖定狀態。

如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶佔了,如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,自旋失敗後要膨脹爲重量級鎖,Mark Word中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態。

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

5. 偏向鎖

大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了。

當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中,如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作(例如Locking、Unlocking及對Mark Word的Update等)。當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。

也就是說,偏向鎖會偏向第一個獲得它的線程,只有當其它線程嘗試競爭偏向鎖時,偏向模式纔會失效。偏向鎖是爲了避免某個線程反覆執行獲取、釋放同一把鎖時的性能消耗,即如果仍是同個線程去獲得這個鎖,偏向鎖模式會直接進入同步塊,不需要再次獲得鎖。

鎖的作用效果

偏向鎖是爲了避免某個線程反覆執行獲取、釋放同一把鎖時的性能消耗,而輕量級鎖和自旋鎖是爲了避免重量級鎖,因爲重量級鎖屬於操作系統層面的互斥操作,掛起和喚醒線程是非常消耗資源的操作。

鎖獲取過程

最終,鎖的獲取過程會是,首先會嘗試輕量級鎖,輕量級鎖會使用CAS操作來獲得鎖,如果輕量級鎖獲得失敗,說明存在多線程對鎖資源的競爭。此時會會嘗試自旋鎖,如果自旋失敗,最終只能膨脹爲重量級鎖。

除重量級鎖外,以上鎖均爲樂觀鎖。

注:本文內容參考自:《深入理解java虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章