如何優化性能:java
由於鎖是串行的這會引發大量的阻塞:因此咱們在使用鎖的時候要儘可能的作到如下幾點:算法
使用分離鎖能夠增長併發訪問容器的量.這可使容器併發的get等相同的操做:編程
競爭鎖是形成多線程應用程序性能瓶頸的主要緣由:數組
區分競爭鎖和非競爭鎖對性能的影響很是重要。若是一個鎖自始至終只被一個線程使用,那麼 JVM 有能力優化它帶來的絕大部分損耗。若是一個鎖被多個線程使用過,可是在任意時刻,都只有一個線程嘗試獲取鎖,那麼它的開銷要大一些。咱們將以上兩種鎖稱爲非競爭鎖。而對性能影響最嚴重的狀況出如今多個線程同時嘗試獲取鎖時。這種狀況是 JVM 沒法優化的,並且一般會發生從用戶態到內核態的切換。現代 JVM 已對非競爭鎖作了不少優化,使它幾乎不會對性能形成影響。常見的優化有如下幾種。緩存
所以,不要過度擔憂非競爭鎖帶來的開銷,要關注那些真正發生了鎖競爭的臨界區中性能的優化。安全
下降鎖競爭的方法:數據結構
不少開發人員由於擔憂同步帶來的性能損失,而儘可能減小鎖的使用,甚至對某些看似發生錯誤機率極低的臨界區不使用鎖保護。這樣作每每不會帶來性能提升,還會引入難以調試的錯誤。由於這些錯誤一般發生的機率極低,並且難以重現。多線程
所以,在保證程序正確性的前提下,解決同步帶來的性能損失的第一步不是去除鎖,而是下降鎖的競爭。一般,有如下三類方法能夠下降鎖的競爭:減小持有鎖的時間,下降請求鎖的頻率,或者用其餘協調機制取代獨佔鎖。這三類方法中包含許多最佳實踐,在下文中將一一介紹。併發
避免在臨界區中進行耗時計算: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併發編程實踐裏面有解釋
公平鎖,就是很公平,在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,若是爲空,或者當前線程線程是等待隊列的第一個,就佔有鎖,不然就會加入到等待隊列中,之後會按照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; }