1. 程式人生 > >鎖優化(5種方法)

鎖優化(5種方法)

1. 鎖優化的思路和方法

鎖優化的思路和方法有以下幾種:

  • 減少鎖持有時間
  • 減小鎖粒度
  • 鎖分離
  • 鎖粗化
  • 鎖消除

1.1 減少鎖持有時間

public synchronized void syncMethod(){  
        othercode1();  
        mutextMethod();  
        othercode2(); 
    }

像上述程式碼這樣,在進入方法前就要得到鎖,其他執行緒就要在外面等待。

這裡優化的一點在於,要減少其他執行緒等待的時間,所以,只需要在有執行緒安全要求的程式程式碼上加鎖。

public void syncMethod
(){ othercode1(); synchronized(this) { mutextMethod(); } othercode2(); }

1.2 減小鎖粒度

將大物件(這個物件可能會被很多執行緒訪問),拆成小物件,大大增加並行度,降低鎖競爭。降低了鎖的競爭,偏向鎖,輕量級鎖成功率才會提高。

最最典型的減小鎖粒度的案例就是ConcurrentHashMap。

1.3 鎖分離

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

讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離。

比如LinkedBlockingQueue
這裡寫圖片描述

從頭部取出資料,從尾部放入資料,使用兩把鎖。

1.4 鎖粗化

通常情況下,為了保證多執行緒間的有效併發,會要求每個執行緒持有鎖的時間儘量短,即在使用完公共資源後,應該立即釋放鎖。只有這樣,等待在這個鎖上的其他執行緒才能儘早的獲得資源執行任務。

但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於效能的優化 。

舉個例子:

public void demoMethod(){  
        synchronized(lock){   
            //do sth.  
        }  
        //...
做其他不需要的同步的工作,但能很快執行完畢 synchronized(lock){ //do sth. } }

這種情況,根據鎖粗化的思想,應該合併:

public void demoMethod(){  
        //整合成一次鎖請求 
        synchronized(lock){   
            //do sth.   
            //...做其他不需要的同步的工作,但能很快執行完畢  
        }
    }

當然這是有前提的,前提就是中間的那些不需要同步的工作是很快執行完成的。

再舉一個極端的例子:

for(int i = 0; i < CIRCLE; i++){  
            synchronized(lock){  
                 //...
            } 
        }

在一個迴圈內不同得獲得鎖。雖然JDK內部會對這個程式碼做些優化,但是還不如直接寫成:

synchronized(lock){ 
            for(int i=0;i<CIRCLE;i++){ 

            } 
        }

當然如果有需求說,這樣的迴圈太久,需要給其他執行緒不要等待太久,那隻能寫成上面那種。如果沒有這樣類似的需求,還是直接寫成下面那種比較好。

1.5 鎖消除

鎖消除是在編譯器級別的事情。

在即時編譯器時,如果發現不可能被共享的物件,則可以消除這些物件的鎖操作。

也許你會覺得奇怪,既然有些物件不可能被多執行緒訪問,那為什麼要加鎖呢?寫程式碼時直接不加鎖不就好了。

但是有時,這些鎖並不是程式設計師所寫的,有的是JDK實現中就有鎖的,比如Vector和StringBuffer這樣的類,它們中的很多方法都是有鎖的。當我們在一些不會有執行緒安全的情況下使用這些類的方法時,達到某些條件時,編譯器會將鎖消除來提高效能。

比如:

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

    public static String createStringBuffer(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

上述程式碼中的StringBuffer.append是一個同步操作,但是StringBuffer卻是一個區域性變數,並且方法也並沒有把StringBuffer返回,所以不可能會有多執行緒去訪問它。

那麼此時StringBuffer中的同步操作就是沒有意義的。

開啟鎖消除是在JVM引數上設定的,當然需要在server模式下:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

並且要開啟逃逸分析。 逃逸分析的作用呢,就是看看變數是否有可能逃出作用域的範圍。

比如上述的StringBuffer,上述程式碼中craeteStringBuffer的返回是一個String,所以這個區域性變數StringBuffer在其他地方都不會被使用。如果將craeteStringBuffer改成

public static StringBuffer craeteStringBuffer(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }

那麼這個 StringBuffer被返回後,是有可能被任何其他地方所使用的(譬如被主函式將返回結果put進map啊等等)。那麼JVM的逃逸分析可以分析出,這個區域性變數 StringBuffer逃出了它的作用域。

所以基於逃逸分析,JVM可以判斷,如果這個區域性變數StringBuffer並沒有逃出它的作用域,那麼可以確定這個StringBuffer並不會被多執行緒所訪問,那麼就可以把這些多餘的鎖給去掉來提高效能。

當JVM引數為:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

輸出:

craeteStringBuffer: 302 ms

JVM引數為:

-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks

輸出:

craeteStringBuffer: 660 ms

顯然,鎖消除的效果還是很明顯的。