多線程性能問題

如何優化性能:java

  1. 若是重複計算量大的話,使用緩存來保存舊的結果,以便下次計算時使用;
  2. 減小阻塞,運行和阻塞會增長上下文切換。

由於鎖是串行的這會引發大量的阻塞:因此咱們在使用鎖的時候要儘可能的作到如下幾點:算法

  1. 減小鎖的持有時間(儘可能使用synchronized或者顯示鎖將沒必要要同步的代碼移除加鎖的範圍內,但不要把一個synchronized塊分拆成多個塊這會拔苗助長);
  2. 減小請求鎖的操做;
  3. 使用協調機制取代獨佔所。

使用分離鎖能夠增長併發訪問容器的量.這可使容器併發的get等相同的操做:編程

  1. 分離鎖的實現:
    • 使用多個Object做爲synchronized的代碼塊的"對象鎖"
    • 在取出或者寫入的利用 獲取對象的hash%鎖數量 隨即便用一個"對象鎖"
  2. 分離鎖:其實就是使用一個Array保存固定數量的Object(其實隨便任意對象)使用這些對象的鎖做爲"分離鎖".而後在get/put等操做中隨即便用其中任意一個鎖.

競爭鎖是形成多線程應用程序性能瓶頸的主要緣由:數組

        區分競爭鎖和非競爭鎖對性能的影響很是重要。若是一個鎖自始至終只被一個線程使用,那麼 JVM 有能力優化它帶來的絕大部分損耗。若是一個鎖被多個線程使用過,可是在任意時刻,都只有一個線程嘗試獲取鎖,那麼它的開銷要大一些。咱們將以上兩種鎖稱爲非競爭鎖。而對性能影響最嚴重的狀況出如今多個線程同時嘗試獲取鎖時。這種狀況是 JVM 沒法優化的,並且一般會發生從用戶態到內核態的切換。現代 JVM 已對非競爭鎖作了不少優化,使它幾乎不會對性能形成影響。常見的優化有如下幾種。緩存

  1. 若是一個鎖對象只能由當前線程訪問,那麼其餘線程沒法得到該鎖併發生同步 , 所以 JVM 能夠去除對這個鎖的請求。
  2. 逸出分析 (escape analysis) 能夠識別本地對象的引用是否在堆中被暴露。若是沒有,就能夠將本地對象的引用變爲線程本地的 (thread local) 。
  3. 編譯器還能夠進行鎖的粗化 (lock coarsening) 。把鄰近的 synchronized 塊用相同的鎖合併起來,以減小沒必要要的鎖的獲取和釋放。

        所以,不要過度擔憂非競爭鎖帶來的開銷,要關注那些真正發生了鎖競爭的臨界區中性能的優化。安全

下降鎖競爭的方法:數據結構

        不少開發人員由於擔憂同步帶來的性能損失,而儘可能減小鎖的使用,甚至對某些看似發生錯誤機率極低的臨界區不使用鎖保護。這樣作每每不會帶來性能提升,還會引入難以調試的錯誤。由於這些錯誤一般發生的機率極低,並且難以重現。多線程

        所以,在保證程序正確性的前提下,解決同步帶來的性能損失的第一步不是去除鎖,而是下降鎖的競爭。一般,有如下三類方法能夠下降鎖的競爭:減小持有鎖的時間,下降請求鎖的頻率,或者用其餘協調機制取代獨佔鎖。這三類方法中包含許多最佳實踐,在下文中將一一介紹。併發

避免在臨界區中進行耗時計算:app

        一般使代碼變成線程安全的技術是給整個函數加上一把「大鎖」。例如在 Java 中,將整個方法聲明爲 synchronized 。可是,咱們須要保護的僅僅是對象的共享狀態,而不是代碼。

        過長時間的持有鎖會限制應用程序的可擴展性。 Brian Goetz 在《 Java Concurrency in Practice 》一書中提到,若是一個操做持有鎖的時間超過 2 毫秒,而且每個操做都須要這個鎖,那麼不管有多少個空閒處理器,應用程序的吞吐量都不會超過每秒 500 個操做。若是可以減小持有這個鎖的時間到 1 毫秒,就能將這個與鎖相關的吞吐量提升到每秒 1000 個操做。事實上,這裏保守地估計了過長時間持有鎖的開銷,由於它並無計算鎖的競爭帶來的開銷。例如,由於獲取鎖失敗帶來的忙等和線程切換,都會浪費 CPU 時間。減少鎖競爭發生可能性的最有效方式是儘量縮短持有鎖的時間。這能夠經過把不須要用鎖保護的代碼移出同步塊來實現, 尤爲是那些花費「昂貴」的操做,以及那些潛在的阻塞操做,好比 I/O 操做。

        在例 1 中,咱們使用 JLM(Java Lock Monitor) 查看 Java 中鎖使用的狀況。 foo1 使用 synchronized 保護整個函數,foo2 僅保護變量 maph 。 AVER_HTM 顯示了每一個鎖的持有時間。能夠看到將無關語句移出同步塊後,鎖的持有時間下降了,而且程序執行時間也縮短了。

例 1. 避免在臨界區中進行耗時計算

public class TimeConsumingLock implements Runnable { 
    private final Map<String, String> maph = new HashMap<String, String>(); 

    private int opNum; 
    public TimeConsumingLock(int on) { 
        opNum = on; 
    } 
        
    public synchronized void foo1(int k) { 
        String key = Integer.toString(k); 
        String value = key+"value"; 
        if (null == key) 
        { 
            return ; 
        }else { 
            maph.put(key, value);        
        } 
    }    
        
    public void foo2(int k) { 
        String key = Integer.toString(k); 
        String value = key+"value"; 
        if (null == key) 
        { 
            return ; 
        }else { 
            synchronized(this){ 
                maph.put(key, value); 
            } 
        } 
    }    
        
    public void run(){ 
        for (int i=0; i<opNum; i++) 
        {        
            //foo1(i);  //Time consuming 
            foo2(i);  //This will be better 
        } 
    } 
 }

使用 foo1 的結果
 MON-NAME [08121048] TimeConsumingLock@D7968DB8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  5318465  5318465       35        0 349190349  8419428    38     5032  

 Execution Time: 16106 milliseconds 

 使用 foo2 的結果
 MON-NAME [D594C53C] TimeConsumingLock@D6DD67B0 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  5635938  5635938       71        0 373087821  8968423    27     3322 
     
 Execution Time: 12157  milliseconds

分拆鎖和分離鎖:

        下降鎖競爭的另外一種方法是下降線程請求鎖的頻率。分拆鎖 (lock splitting) 和分離鎖 (lock striping) 是達到此目的兩種方式。相互獨立的狀態變量,應該使用獨立的鎖進行保護。有時開發人員會錯誤地使用一個鎖保護全部的狀態變量。這些技術減少了鎖的粒度,實現了更好的可伸縮性。可是,這些鎖須要仔細地分配,以下降發生死鎖的危險。

        若是一個鎖守護多個相互獨立的狀態變量,你可能可以經過分拆鎖,使每個鎖守護不一樣的變量,從而改進可伸縮性。經過這樣的改變,使每個鎖被請求的頻率都變小了。分拆鎖對於中等競爭強度的鎖,可以有效地把它們大部分轉化爲非競爭的鎖,使性能和可伸縮性都獲得提升。

        在例 2 中,咱們將原先用於保護兩個獨立的對象變量的鎖分拆成爲單獨保護每一個對象變量的兩個鎖。在 JLM 結果中,能夠看到原先的一個鎖 SplittingLock@D6DD3078 變成了兩個鎖 java/util/HashSet@D6DD7BE0 和 java/util/HashSet@D6DD7BE0 。而且申請鎖的次數 (GETS) 和鎖的競爭程度 (SLOW, TIER2, TIER3) 都大大下降了。最後,程序的執行時間由 12981 毫秒降低到 4797 毫秒。

        當一個鎖競爭激烈時,將其分拆成兩個,極可能獲得兩個競爭激烈的鎖。儘管這可使兩個線程併發執行,從而對可伸縮性有一些小的改進。但仍然不能大幅地提升多個處理器在同一個系統中的併發性。

        分拆鎖有時候能夠被擴展,分紅若干加鎖塊的集合,而且它們歸屬於相互獨立的對象,這樣的狀況就是分離鎖。例如,ConcurrentHashMap 的實現使用了一個包含 16 個鎖的數組,每個鎖都守護 HashMap 的 1/16 。假設 Hash 值均勻分佈,這將會把對於鎖的請求減小到約爲原來的 1/16 。這項技術使得 ConcurrentHashMap 可以支持 16 個的併發 Writer 。當多處理器系統的大負荷訪問須要更好的併發性時,鎖的數量還能夠增長。

        在例 3 中,咱們模擬了 ConcurrentHashMap 中使用分離鎖的狀況。使用 4 個鎖保護數組的不一樣部分。在 JLM 結果中,能夠看到原先的一個鎖 StrippingLock@D79962D8 變成了四個鎖 java/lang/Object@D79964B8 等。而且鎖的競爭程度 (TIER2, TIER3) 都大大下降了。最後,程序的執行時間由 5536 毫秒降低到 1857 毫秒。

例 2. 分拆鎖

public class SplittingLock implements Runnable{ 
    private final Set<String> users = new HashSet<String>(); 
    private final Set<String> queries = new HashSet<String>(); 
        private int opNum; 
    public SplittingLock(int on) { 
        opNum = on; 
    } 
    
    public synchronized void addUser1(String u) { 
        users.add(u); 
    } 
    
    public synchronized void addQuery1(String q) { 
        queries.add(q); 
    } 
    
    public void addUser2(String u) { 
        synchronized(users){ 
            users.add(u); 
        } 
    } 
    
    public void addQuery2(String q) { 
        synchronized(queries){ 
            queries.add(q); 
        } 
    } 
    
    public void run() { 
        for (int i=0; i<opNum; i++) { 
            String user = new String("user"); 
            user+=i; 
            addUser1(user); 
            
            String query = new String("query"); 
            query+=i; 
            addQuery1(query); 
        } 
    } 
 }

使用 addUser1 和 addQuery1 的結果
 
 MON-NAME [D5848CB0] SplittingLock@D6DD3078 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM 
     0  9004711  9004711      101        0 482982391 10996987    44     3393  

 Execution Time: 12981 milliseconds 

 使用 addUser2 和 addQuery2 的結果    
  
 MON-NAME [D5928C98] java/util/HashSet@D6DD7BE0 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  1875510  1875510       38        0 108706364  2546875    14     5173  
     
 MON-NAME [D5928C98] java/util/HashSet@D6DD7BE0 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0   272365   272365        0        0 15154239   352397     1     3042 
     
 Execution Time: 4797  milliseconds

例 3. 分離鎖

public class StrippingLock implements Runnable{ 
    private final Object[] locks; 
    private static final int N_LOCKS = 4; 
    private final String [] share ; 
    private int opNum; 
    private int N_ANUM; 

    public StrippingLock(int on, int anum) { 
        opNum = on; 
        N_ANUM = anum; 
        share = new String[N_ANUM]; 
        locks = new Object[N_LOCKS]; 
        for (int i = 0; i<N_LOCKS; i++) 
        locks[i] = new Object(); 
    } 

    public synchronized void put1(int indx, String k) { 
        share[indx] = k;    //acquire the object lock         
    } 

      
    public void put2(int indx, String k) { 
        synchronized (locks[indx%N_LOCKS]) { 
            share[indx] = k;    // acquire the corresponding lock 
        } 
    } 
    
    public void run() 
    { 
        //The expensive put 
        /*for (int i=0; i<opNum; i++) 
        { 
            put1(i%N_ANUM, Integer.toString(i+1)); 
        }*/ 
        //The cheap put 
        for (int i=0; i<opNum; i++) 
        { 
            put2(i%N_ANUM, Integer.toString(i+1)); 
        } 
    }    
 }

使用 put1 的結果
 MON-NAME [08121228] StrippingLock@D79962D8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM 
     0  4830690  4830690      460        0 229538313  5010789    18     2552  

 Execution Time: 5536 milliseconds 

 使用 put2 的結果
 MON-NAME [08121388] java/lang/Object@D79964B8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  4591046  4591046     1517        0 151042525  3016162    13     1925  
     
 MON-NAME [08121330] java/lang/Object@D79964C8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  1717579  1717579      523        0 50596994   958796     5     1901  
     
 MON-NAME [081213E0] java/lang/Object@D79964D8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  1814296  1814296      536        0 58043786  1113454     5     1799  
     
 MON-NAME [08121438] java/lang/Object@D79964E8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  3126427  3126427      901        0 96627408  1857005     9     1979  
     
 Execution Time: 1857  milliseconds

避免熱點域

        在某些應用中,咱們會使用一個共享變量緩存經常使用的計算結果。每次更新操做都須要修改該共享變量以保證其有效性。例如,隊列的 size,counter,鏈表的頭節點引用等。在多線程應用中,該共享變量須要用鎖保護起來。這種在單線程應用中經常使用的優化方法會成爲多線程應用中的「熱點域 (hot field) 」,從而限制可伸縮性。若是一個隊列被設計成爲在多線程訪問時保持高吞吐量,那麼能夠考慮在每一個入隊和出隊操做時不更新隊列 size 。 ConcurrentHashMap 中爲了不這個問題,在每一個分片的數組中維護一個獨立的計數器,使用分離的鎖保護,而不是維護一個全局計數。

獨佔鎖的替代方法

        用於減輕競爭鎖帶來的性能影響的第三種技術是放棄使用獨佔鎖,而使用更高效的併發方式管理共享狀態。例如併發容器,讀 - 寫鎖,不可變對象,以及原子變量。

        java.util.concurrent.locks.ReadWriteLock 實現了一個多讀者 - 單寫者鎖:多個讀者能夠併發訪問共享資源,可是寫者必須獨佔得到鎖。對於多數操做都爲讀操做的數據結構,ReadWriteLock 比獨佔鎖提供更好的併發性。

        原子變量提供了避免「熱點域」更新致使鎖競爭的方法,如計數器、序列發生器、或者對鏈表數據結構頭節點引用的更新。

        在例 4 中,咱們使用原子操做更新數組的每一個元素,避免使用獨佔鎖。程序的執行時間由 23550 毫秒降低到 842 毫秒。

例 4. 使用原子操做的數組

public class AtomicLock implements Runnable{ 
    private final long d[]; 
    private final AtomicLongArray a; 
        private int a_size; 
     
    public AtomicLock(int size) { 
        a_size = size; 
        d = new long[size]; 
        a = new AtomicLongArray(size); 
    } 
 
    public synchronized void set1(int idx, long val) { 
        d[idx] = val; 
    } 
 
    public synchronized long get1(int idx) { 
        long ret = d[idx]; 
        return ret; 
    } 
   
    public void set2(int idx, long val) { 
        a.addAndGet(idx, val);   
    } 
 
    public long get2(int idx) { 
        long ret = a.get(idx); 
        return ret; 
    } 
     
    public void run() { 
        for (int i=0; i<a_size; i++) {     
            //The slower operations 
            //set1(i, i); 
            //get1(i); 
             
            //The quicker operations 
            set2(i, i); 
            get2(i); 
        } 
    }     
 }

set1 和 get1 的結果  
Execution Time: 23550 milliseconds 

set2 和 get2 的結果    
Execution Time: 842 milliseconds

使用併發容器

        從 Java1.5 開始,java.util.concurrent 包提供了高效地線程安全的併發容器。併發容器自身保證線程安全性,同時爲經常使用操做在大量線程訪問的狀況下作了優化。這些容器適合在多核平臺上運行的多線程應用中使用,具備高性能和高可擴展性。 Amino 項目提供的更多的高效的併發容器和算法。

使用 Immutable 數據和 Thread Local 的數據

        Immutable 數據在其生命週期中始終保持不變,因此能夠安全地在每一個線程中複製一份以便快速讀取。

        ThreadLocal 的數據,只被線程自己鎖使用,所以不存在不一樣線程之間的共享數據的問題。 ThrealLocal 能夠用來改善許多現有的共享數據。例如全部線程共享的對象池、等待隊列等,能夠變成每一個 Thread 獨享的對象池和等待隊列。採用 Work-stealing scheduler 代替傳統的 FIFO-Queue scheduler 也是使用 Thread Local 數據的例子。

公平鎖和非公平鎖

        jdk1.5併發包中ReentrantLock的建立能夠指定構造函數的boolean類型來獲得公平鎖或非公平鎖,關於二者區別,java併發編程實踐裏面有解釋

  1. 公平鎖:   Threads acquire a fair lock in the order in which they requested it
  2. 非公平鎖:a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.

公平鎖,就是很公平,在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,若是爲空,或者當前線程線程是等待隊列的第一個,就佔有鎖,不然就會加入到等待隊列中,之後會按照FIFO的規則從隊列中取到本身。
非公平鎖比較粗魯,上來就直接嘗試佔有鎖,若是嘗試失敗,就再採用相似公平鎖那種方式。

//公平獲取鎖
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        //狀態爲0,說明當前沒有線程佔有鎖
        if (c == 0) {
            //若是當前線程是等待隊列的第一個或者等待隊列爲空,則經過cas指令設置state爲1,當前線程得到鎖
            if (isFirst(current)
                    && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } //若是當前線程自己就持有鎖,那麼疊加狀態值,持續得到鎖
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) {
                throw new Error("Maximum lock count exceeded");
            }
            setState(nextc);
            return true;
        }
        //以上條件都不知足,那麼線程進入等待隊列。
        return false;
    }
//非公平獲取鎖
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //若是當前沒有線程佔有鎖,當前線程直接經過cas指令佔有鎖,管他等待隊列,就算本身排在隊尾也是這樣
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
            {
                throw new Error("Maximum lock count exceeded");
            }
            setState(nextc);
            return true;
        }
        return false;
    }
相關文章
相關標籤/搜索