同步鎖的優化思路及JDK對鎖的一些優化

一、鎖優化的思路和方法

主要有以下5點:
  • 減少鎖持有時間
    主要就是精準的確定需要加鎖的模塊,不需要加鎖的代碼模塊分離在同步塊之外。

  • 減小鎖粒度
    將大對象(這個對象可能會被很多線程訪問),拆成小對象,大大增加並行度,降低鎖競爭。降低了鎖的競爭,偏向鎖,輕量級鎖成功率纔會提高。最最典型的減小鎖粒度的案例就是ConcurrentHashMap

  • 鎖分離
    最常見的鎖分離就是讀寫鎖ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了線程安全,又提高了性能。

  • 鎖粗化
    簡而言之就是將一系列連續的加鎖模塊,用同一個鎖進行管理,可以避免線程頻繁的進行加鎖/解鎖操作。

  • 鎖消除
    鎖消除是在編譯器級別的事情。涉及Java逃逸分析技術,下面內容詳細說一下哈。

二、JDK對鎖的一些優化介紹:

1、 偏向鎖

鎖偏向是一種針對加鎖操作的優化手段。
如果一個線程獲得了鎖,那麼鎖就進入偏向模式。當這個線程再次請求鎖時,無須再做任何同步操作。這樣就節省了大量有關鎖申請的操作,從而提高了程序性能。

因此,對於幾乎沒有鎖競爭的場合,偏向鎖有比較好的優化效果,因爲連續多次極有可能是同一個線程請求相同的鎖。而對於鎖競爭比較激烈的場合,其效果不佳。因爲在競爭激烈的場合,最有可能的情況是每次都是不同的線程來請求相同的鎖。

JVM默認啓用偏向鎖 -XX:+UseBiasedLocking

2、 輕量級鎖

如果偏向鎖失敗,即上一個請求的鎖的線程和這個線程不是同一個。偏向鎖失敗意味者不能避免做同步操作。此時,虛擬機並不會立即掛起線程。他會使用一種成爲輕量級鎖的優化手段。

輕量級鎖是一種快速的鎖定方法,在進入互斥之前,使用CAS操作來嘗試加鎖,儘量不要用操作系統層面的互斥,提高了性能。

輕量級鎖的操作也很方便,它只是簡單地將對象頭部作爲指針,指向持有鎖的線程堆棧的內部,來判斷一個線程是否持有對象鎖。 如果線程獲得輕量級鎖成功,則可以順利進入臨界區。如果輕量級鎖失敗,則表示其他線程搶先爭奪了鎖,那麼當前線程的鎖請求就會膨脹爲重量級鎖。

3、 自旋鎖

鎖膨脹後,虛擬機爲了避免線程真實地在操作系統層面掛起,虛擬機還會在做最後的努力–自旋鎖

由於當前線程暫時無法獲得鎖,但是什麼時候可以獲得鎖是一個未知數。也許在CPU幾個時鐘週期後,就可以得到鎖。如果這樣,簡單粗暴的掛起線程可能是一種得不償失的操作,因此係統會進行一次賭注:它會假設在不久的將來,線程可以得到這把鎖。因此虛擬機讓當前線程做個空循環,在經過若干次循環後,如果可以得到鎖,那麼就順利進入臨界區。如果還不能得到鎖,纔會真實地將線程在操作系統層面掛起。

如果同步塊很長,自旋失敗,會降低系統性能。如果同步塊很短,自旋成功,節省線程掛起切換時間,提升系統性能。利弊還是要自己判斷!

JDK1.6中-XX:+UseSpinning開啓。JDK1.7中,去掉此參數,改爲內置實現 。

TIP:小總結

首先偏向鎖是爲了避免某個線程反覆獲得/釋放同一把鎖時的性能消耗,
如果仍然是同個線程去獲得這個鎖,嘗試偏向鎖時會直接進入同步塊,不需要再次獲得鎖。

而輕量級鎖和自旋鎖都是爲了避免直接調用操作系統層面的互斥操作,因爲掛起線程是一個很耗資源的操作。

4、 鎖消除

鎖消除是一種更徹底的鎖優化,鎖消除是在編譯器級別的事情。Java虛擬機在JIT編譯時,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖。通過鎖消除,可以節省毫無意義的請求鎖時間。

下面這種這種情況,我們使用Vector, 而Vector內部使用了synchronize請求鎖。

public String []  createStrings(){
    Vector<String>  v = new Vector<String>();
    for(int i=0; i<100; i++){
        v.add(Integer.toString(i));
    }
    return v.toArray(new String[]{});
}

由於V只在函數createStrnigs中使用,因此它只是一個單純的局部變量。局部變量是在線程棧上分配的,屬於線程私有額數據,因此不可能被其他線程訪問。所以,在這種情況下,Vector內部所有加鎖同步都是沒有必要的。如果虛擬機檢測到這種情況,就會將這些無用的鎖操作去除。

鎖消除涉及的一項關鍵技術爲逃逸分析
所謂逃逸分析就是觀察某一個變量是否會逃出某一個作用域

在本例中,變量v顯然沒有逃出createString 函數之外。以此爲基礎,虛擬機纔可以大膽的將v內部的加鎖操作去除。如果createStrings返回的不是String數組,而是v本身,那麼就認爲變量v逃逸出了當前函數,也就是說v有可能被其他線程訪問。如是這樣,虛擬機就不能消除v中的鎖操作。

在-server模式下打開逃逸分析鎖消除(+表示打開,-表示關閉)參數爲:-XX:DoEscapeAnalysis -XX:+EliminateLocks

對比一下開啓鎖消除與否,相同代碼的耗時情況:

public class EliminateLockDemo {
    public static void main(String[] args) {
        long start=System.currentTimeMillis();
        for (int i=0;i<20000000;i++){
            createStringBuffer("JVM","Diagnosis");
        }
        long bufferCost=System.currentTimeMillis()-start;
        System.out.println("createStringBuffer:"+bufferCost+" ms");
    }

    private static String createStringBuffer(String jvm, String diagnosis) {
        StringBuffer stringBuffer=new StringBuffer();
        stringBuffer.append(jvm);
        stringBuffer.append(diagnosis);
        return stringBuffer.toString();
    }
}

jvm配置-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
對應耗時:

createStringBuffer:1166 ms

jvm配置爲-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks
對應耗時:

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