#1,避免死鎖前端
死鎖問題是多線程的特有問題,它能夠認爲是線程間切換消耗系統性能的一種極端狀況。java
在死鎖時,線程間相互等待資源,而又不釋放自身的資源,致使無窮無盡的等待,其結果是系統任務永遠沒法執行完成。數據結構
死鎖問題是在多線程開發中應該堅定避免和杜絕的問題。多線程
通常來講,要出現死鎖問題須要知足如下條件:併發
@互斥條件:一個資源每次只能被一個線程使用。app
@請求與保持條件:一個線程因請求資源而阻塞時,對已得到的資源保持不放。less
@不剝奪條件:線程已得到的資源,在未使用完以前,不能強行剝奪。
async
@循環等待條件:若干線程之間造成一種頭尾相接的循環等待資源關係。函數
只要破壞死鎖4個必要條件中的任何一個,死鎖問題就能得以解決。高併發
#2,減少鎖持有時間
對於使用鎖進行併發控制的應用程序而言,在鎖競爭過程當中,單個線程對鎖的持有時間與系統性能有着直接的關係。
若是線程持有鎖的時間很長,那麼相對地,鎖的競爭程度也就越激烈。所以,在程序開發過程當中,應該儘量地減小對某個鎖的佔有時間,以減小線程間互斥的可能。
public synchronized void syncMethod(){ method1();//比較耗時 coreMethod(); method2();//比較耗時 }
在syncMethod()方法中,假設只有coreMethod()方法是有同步須要的,而method1()和method2()分別是重量級的方法,則會花費較長的CPU時間。此時,若是併發量大,使用這種對整個方法作同步的方案,會致使等待線程大量增長。由於一個線程,在進入該方法時得到內部鎖,只有再全部任務都執行完成後,纔會釋放鎖。
一個較爲優化的解決方法時,只在必要時進行同步,這樣就能明顯減小線程持有鎖的時間,提升系統的吞吐量。
public void syncMethod(){ method1();//比較耗時 synchronized(this){ coreMethod(); } method2();//比較耗時 }
在改進的代碼中,只針對coreMethod()方法作了同步,鎖佔用的時間相對較短,所以能有更高的並行度。
減小鎖的持有時間有助於下降鎖衝突的可能性,進而提高系統的併發能力。
#3.減少鎖粒度
減少鎖粒度也是一種削弱多線程鎖競爭的一種有效手段,這種技術典型的使用場景就是ConcurrentHashMap類的實現。
做爲JDK併發包中重要的成員類,很好地使用了拆分鎖對象的方式提升ConcurrentHashMap的吞吐量。ConcurrentHashMap將整個HashMap分紅若干個段(Segment),每一個段都是一個子HashMap。
若是須要在ConcurrentHashMap中增長一個新的表項,並非將整個HashMap加鎖,而是首先根據hashcode獲得該表項應該被存放到哪一個段中,而後對該段加鎖,並完成put()操做。在多線程環境中,若是多個線程同時進行put()操做,只要被加入的表項不存放在同一個段中,則線程間即可以作到真正的並行。
默認狀況下,ConcurrentHashMap擁有16個段,所以,若是夠幸運的話,ConcurrentHashMap能夠同時接受16個線程同時插入(若是都插入不一樣的段中),從而大大提升其吞吐量。
可是,減小鎖粒度會引入一個新的問題,即:當系統須要取得全局鎖時,其消耗的資源會比較多。仍然以ConcurrentHashMap類爲例,雖然其put()方法很好地分離了鎖,可是當試圖訪問ConcurrentHashMap全局信息時,就須要同時取得全部段的鎖方能順利實施。好比ConcurrentHashMap的size()方法,它將返回ConcurrentHashMap的有效表項的數量,即ConcurrentHashMap的所有有效表項之和。要獲取這個信息須要取得全部字段的鎖,所以,可參考其size()的代碼以下:
public int size() { final Segment<K,V>[] segments = this.segments; long sum = 0; long check = 0; int[] mc = new int[segments.length]; // Try a few times to get accurate count. On failure due to // continuous async changes in table, resort to locking. for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { check = 0; sum = 0; int mcsum = 0; for (int i = 0; i < segments.length; ++i) { sum += segments[i].count; mcsum += mc[i] = segments[i].modCount; } if (mcsum != 0) { for (int i = 0; i < segments.length; ++i) { check += segments[i].count; if (mc[i] != segments[i].modCount) { check = -1; // force retry break; } } } if (check == sum) break; } if (check != sum) { // Resort to locking all segments sum = 0; for (int i = 0; i < segments.length; ++i) segments[i].lock(); for (int i = 0; i < segments.length; ++i) sum += segments[i].count; for (int i = 0; i < segments.length; ++i) segments[i].unlock(); } if (sum > Integer.MAX_VALUE) return Integer.MAX_VALUE; else return (int)sum; }
能夠看到代碼在後面計算總數時,先要得到全部段的鎖,而後再求和。可是,ConcurrentHashMap的size()方法並不老是這樣執行,事實上,size()方法會先使用無鎖的方式求和,若是失敗纔會嘗試這種加鎖方法。但無論怎麼說,在高併發場合ConcurrentHashMap的size()的性能依然要差於同步的HashMap.
所以,只有再相似於size()獲取全局信息的方法調用並不頻繁時,這種減少鎖粒度的方法才能真正意義上提升系統吞吐量。
所謂減少鎖粒度,就是指縮小鎖定對象的範圍,(能夠鎖定實例對象的,就不鎖定class類對象)從而減小鎖衝突的可能性,進而提升系統的併發能力。
#4,讀寫分離鎖類替換獨佔鎖
使用讀寫分離鎖來替代獨佔鎖是減少鎖粒度的一種特殊狀況。若是說上面的減小鎖粒度是經過分割數據結構實現的,那麼,讀寫鎖則是對系統功能點的分割。
由於讀操做自己不會影響數據的完整性和一致性,所以,理論上講,在大部分狀況下,應該能夠容許多線程同時讀。
在讀多寫少的場合,使用讀寫鎖能夠有效提高系統的併發能力。
#5,鎖分離
讀寫鎖思想的延伸就是鎖分離,讀寫鎖根據讀寫操做功能上的不一樣,進行了有效的鎖分離。依據應用程序的功能特色,使用相似的分離思想,也能夠對獨佔鎖進行分離。
以LinkedBlockingQueue來講,take()和put()分別實現了從隊列中取得數據和往隊列中增長數據的功能。雖然兩個函數都對當前隊列進行了修改操做,但因爲LinkedBlockingQueue是基於鏈表的,所以,兩個操做分別做用於隊列的前端和尾端,從理論上說,二者並不衝突。
若是使用獨佔鎖,則要求在兩個操做進行時獲取當前隊列的獨佔鎖,那麼take()和put()操做就不可能真正的併發,在運行時,它們會彼此等待對方釋放鎖資源。在這種狀況下,鎖競爭會相對比較激烈,從而影響程序在高併發時的性能。
所以,在JDK實現中,並無採用這樣的方式,取而代之的是兩把不一樣的鎖分離了take()和put()操做。
/** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock(); /** Wait queue for waiting takes */ private final Condition notEmpty = takeLock.newCondition(); /** Lock held by put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock(); /** Wait queue for waiting puts */ private final Condition notFull = putLock.newCondition();
以上代碼片斷定義了takeLock和putLock,所以take()和put()函數相互獨立,它們之間不存在鎖競爭關係。
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
/** * Inserts the specified element at the tail of this queue, waiting if * necessary for space to become available. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from * capacity. Similarly for all other uses of count in * other wait guards. */ while (count.get() == capacity) { notFull.await(); } enqueue(e); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); }
經過takeLock和putLock兩把鎖,LinkedBlockingQueue實現了取數據和寫數據的分離,使二者在真正意義上成爲可併發的操做。
#6,重入鎖(ReentrantLock)和內部鎖(synchronized)
內部鎖和重入鎖有功能上的重複,全部使用內部鎖實現的功能,使用重入鎖均可以實現。從使用上看,內部鎖使用簡單,所以獲得了普遍的使用,重入鎖使用略微複雜,必須在finally代碼中,顯示釋放重入鎖,而內部鎖能夠自動釋放。
從性能上看,在高併發量的狀況下,內部鎖的性能略遜於重入鎖,可是JVM對內部鎖實現了不少優化,而且有理由相信,在未來的JDK版本中,內部鎖的性能會愈來愈好。
從功能上看,重入鎖有着更爲強大的功能,好比提供了鎖等待時間,支持鎖終端和快讀鎖輪詢,這些技術有助於避免死鎖的產生,從而提升系統的穩定性。
同時,衝如梭還提供了一套Condition機制,經過Condition,重入鎖能夠進行復雜的線程控制功能,而相似的功能,內部鎖須要經過Object的wait()和notify()方法實現。
#7,鎖粗化
一般狀況下,爲了保證多線程間的有效併發,會要求每一個線程尺有所短寸有所長的時間儘可能短,即在使用完公共資源後,應該當即釋放鎖。只有這樣,等待在這個鎖上的其餘線程才能儘早地得到資源執行任務。可是,凡事都有一個度,若是堆同一個鎖不停地進行請求,同步和釋放,其自己也會消耗系統寶貴的資源,反而不利於性能的優化。
爲此,JVM在遇到一連串連續的對同一鎖不斷進行請求和釋放的操做時,便會把全部的鎖操做整合成對鎖的一次請求,從而減小對鎖的請求同步次數,這個操做叫作鎖的粗化。
public void syncMethod(){ synchronized(lock){ //TODO } synchronized(lock){ //TODO }
上面的代碼會被整合爲以下形式:
public void syncMethod(){ synchronized(lock){ //TODO //TODO } }
在開發當中,咱們也應該有意識地在合理的場合進行鎖的粗化,尤爲當在循環內請求鎖時。
for (int i = 0; i < CIRCLE; i++) { synchronized(lock){ //TODO } }
將上面的代碼粗化爲:
synchronized(lock){ for (int i = 0; i < CIRCLE; i++) { //TODO } }
#8,自旋鎖
在上面已經提到,線程的狀態和上下文切換是要消耗系統資源的。在多線程比並發時,頻繁的掛起和恢復線程的操做會給系統帶來極大的壓力。特別是當訪問共享資源僅需花費很小一段CPU時間時,鎖的等待可能只須要很短的時間,這段時間可能要比將線程掛起並恢復的時間還要短,所以,爲了這段時間去作重量級的線程切換是不值得的。
爲此,JVM引入了自旋鎖。自旋鎖可使線程在沒有取得鎖時,不被掛起,而轉而去執行一個空循環,在若干個空循環後,線程若是得到了鎖,則繼續執行。若線程依然不能得到鎖,纔會被掛起。
使用自旋鎖後,線程被掛起的概率相對減小,線程執行的連貫性相對增強。所以,對於那些鎖競爭不是很激烈,鎖佔用時間很短的併發線程,是有必定的積極意義,但對於鎖競爭激烈,單線程鎖佔用時間長的併發程序,自旋鎖在自旋等待後,每每依然沒法得到對應的鎖,不只僅白白浪費了CPU時間,最終仍是免不了執行被掛起的操做,反而浪費了系統資源。
JVM虛擬機提供-XX:+UseSpinning參數來開啓自旋鎖,使用-XX:PreBlockSpin參數來設置自旋鎖的等待次數。
#9,鎖消除
鎖消除是JVM在即時編譯時,經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖。經過鎖消除,能夠節省毫無心義的請求鎖時間。
在Java軟件開發過程當中,開發人員必然會使用一些JDK的內置API,好比StringBuffer,Vector等。這些經常使用的工具類可能會被大面積地使用。雖然這些工具類自己可能有對應的非同步版本,可是開發人員也頗有可能在徹底沒有多線程競爭的場合使用他們。
在這種狀況下,這些工具類內部的同步方法就是沒必要要的。JVM虛擬機能夠在運行時,基於逃逸分析技術,捕獲到這些不可能存在競爭卻有申請鎖的代碼段,並消除這些沒必要要的鎖,從而提升系統性能。
public void testStringBuffer(String a, String b){ StringBuffer sb = new StringBuffer(); sb.append(a); sb.append(b); return sb.toString(); }
sb變量的做用域僅限於方法體內部,不可能逃逸出該方法,一次它就不可能被多個線程同時訪問。
逃逸分析和鎖消除分別可使用-XX:+DoEscapeAnalysis和-XX:+EliminateLocks開啓(鎖消除必須工做再-server模式下)。
對鎖的請求和釋放是要消耗系統資源的。使用鎖消除即便能夠去掉那些不可能存在多線程訪問的鎖請求,從而提升系統性能。
#10,鎖偏向
鎖偏向是JDK1.6提出的一種鎖優化方式。其核心思想是,若是程序沒有競爭,則取消以前已經取得鎖的線程同步操做。也就是說,若某一鎖被線程獲取後,便進入偏向模式,當線程再次請求這個鎖時,無需再進行相關的同步操做,從而節省了操做時間,若是在此之間有其餘線程進行了鎖請求,則鎖退出偏向模式。在JVM中使用-XX:+UseBiasedLocking能夠設置啓用偏向鎖。
偏向鎖在鎖競爭激烈的場合沒有優化效果,由於大量的競爭會致使持有鎖的線程不停地切換,鎖也很難一致保持在偏向模式,此時,使用鎖偏向不只得不到性能的提高,反而有損系統性能。所以,在激烈競爭的場合,使用-XX:-UseBiasedLocking參數禁用鎖偏向反而能提高系統吞吐量。