大家使用多線程無非是為了提高效能,但如果多線程使用不當,不但效能提升不明顯,而且會使得資源消耗更大。下面列舉一下可能會造成多線程效能問題的點:
- 死結
- 過多序列化
- 過多鎖競爭
- 切換上下文
- 記憶體同步
下面分別解析以上效能隱患
死結
關於死結,我們在學習作業系統的時候就知道它產生的原因和危害,這裡就不從原理上去累述了,可以從下面的代碼和圖示重溫一下死結產生的原因:
public class LeftRightDeadlock {<br /> private final Object left = new Object();<br /> private final Object right = new Object();<br /> public void leftRight() {<br /> synchronized (left) {<br /> synchronized (right) {<br /> doSomething();<br /> }<br /> }<br /> }<br /> public void rightLeft() {<br /> synchronized (right) {<br /> synchronized (left) {<br /> doSomethingElse();<br /> }<br /> }<br /> }<br />}<br />
預防和處理死結的方法:
1)盡量不要在釋放鎖之前競爭其他鎖
一般可以通過細化同步方法來實現,只在真正需要保護共用資源的地方去拿鎖,並儘快釋放鎖,這樣可以有效降低在同步方法裡調用其他同步方法的情況
2)順序索取鎖資源
如果實在無法避免嵌套索取鎖資源,則需要制定一個索取鎖資源的策略,先規劃好有哪些鎖,然後各個線程按照一個順序去索取,不要出現上面那個例子中不同順序,這樣就會有潛在的死結問題
3)嘗試定時鎖
Java 5提供了更靈活的鎖工具,可以顯式地索取和釋放鎖。那麼在索取鎖的時候可以設定一個逾時時間,如果超過這個時間還沒索取到鎖,則不會繼續堵塞而是放棄此次任務,範例程式碼如下:
public boolean trySendOnSharedLine(String message,<br /> long timeout, TimeUnit unit)<br /> throws InterruptedException {<br /> long nanosToLock = unit.toNanos(timeout)<br /> - estimatedNanosToSend(message);<br /> if (!lock.tryLock(nanosToLock, NANOSECONDS))<br /> return false;<br /> try {<br /> return sendOnSharedLine(message);<br /> } finally {<br /> lock.unlock();<br /> }<br />}<br />
這樣可以有效打破死結條件。
4)檢查死結
JVM採用thread dump的方式來識別死結的方式,可以通過作業系統的命令來向JVM發送thread dump的訊號,這樣可以查詢哪些線程死結。
過多序列化
用多線程實際上就是想並行地做事情,但這些事情由於某些依賴性必須串列工作,導致很多環節得序列化,這實際上很局限系統的可擴充性,就算加CPU加線程,但效能卻沒有線性增長。有個Amdahl定理可以說明這個問題:
其中,F是序列化比例,N是處理器數量,由上可知,只有儘可能減少序列化,才能最大化地提高可擴充能力。降低序列化的關鍵就是降低鎖競爭,當很多並行任務掛在鎖的擷取上,就是序列化的表現
過多鎖競爭
過多鎖競爭的危害是不言而喻的,那麼看看有哪些辦法來降低鎖競爭
1)縮小鎖的範圍
前面也談到這一點,盡量縮小鎖保護的範圍,快進快出,因此盡量不要直接在方法上使用synchronized關鍵字,而只是在真正需要安全執行緒保護的地方使用
2)減小鎖的粒度
Java 5提供了顯式鎖後,可以更為靈活的來保護共用變數。synchronized關鍵字(用在方法上)是預設把整個對象作為鎖,實際上很多時候沒有必要用這麼大一個鎖,這會導致這個類所有synchronized都得串列執行。可以根據真正需要保護的共用變數作為鎖,也可以使用更為精細的策略,目的就是要在真正需要串列的時候串列,舉一個例子:
public class StripedMap {<br /> // Synchronization policy: buckets[n] guarded by locks[n%N_LOCKS]<br /> private static final int N_LOCKS = 16;<br /> private final Node[] buckets;<br /> private final Object[] locks;<br /> private static class Node { ... }<br /> public StripedMap(int numBuckets) {<br /> buckets = new Node[numBuckets];<br /> locks = new Object[N_LOCKS];<br /> for (int i = 0; i < N_LOCKS; i++)<br /> locks[i] = new Object();<br /> }<br /> private final int hash(Object key) {<br /> return Math.abs(key.hashCode() % buckets.length);<br /> }<br /> public Object get(Object key) {<br /> int hash = hash(key);<br /> synchronized (locks[hash % N_LOCKS]) {<br /> for (Node m = buckets[hash]; m != null; m = m.next)<br /> if (m.key.equals(key))<br /> return m.value;<br /> }<br /> return null;<br /> }<br /> public void clear() {<br /> for (int i = 0; i < buckets.length; i++) {<br /> synchronized (locks[i % N_LOCKS]) {<br /> buckets[i] = null;<br /> }<br /> }<br /> }<br /> ...<br />}<br />
上面這個例子是通過hash演算法來把存取的值所對應的hash值來作為鎖,這樣就只需要對hash值相同的對象存取序列化,而不是像HashTable那樣對任何對象任何操作都序列化。
3)減少共用資源的依賴
共用資源是競爭鎖的源頭,在多線程開發中盡量減少對共用資源的依賴,比如對象池的技術應該謹慎考慮,新的JVM對建立對象以做了足夠的最佳化,效能非常好,如果用對象池不但不能提高多少效能,反而會因為鎖競爭導致降低線程的可並發性。
4)使用讀寫分離鎖來替換獨佔鎖
Java 5提供了一個讀寫分離鎖(ReadWriteLock)來實現讀-讀並發,讀-寫串列,寫-寫串列的特性。這種方式更進一步提高了可並發性,因為有些情境大部分是讀操作,因此沒必要串列工作。關於ReadWriteLock的具體使用可以參加一下樣本:
public class ReadWriteMap<K,V> {<br /> private final Map<K,V> map;<br /> private final ReadWriteLock lock = new ReentrantReadWriteLock();<br /> private final Lock r = lock.readLock();<br /> private final Lock w = lock.writeLock();<br /> public ReadWriteMap(Map<K,V> map) {<br /> this.map = map;<br /> }<br /> public V put(K key, V value) {<br /> w.lock();<br /> try {<br /> return map.put(key, value);<br /> } finally {<br /> w.unlock();<br /> }<br /> }<br /> // Do the same for remove(), putAll(), clear()<br /> public V get(Object key) {<br /> r.lock();<br /> try {<br /> return map.get(key);<br /> } finally {<br /> r.unlock();<br /> }<br /> }<br /> // Do the same for other read-only Map methods<br />}<br />
切換上下文
線程比較多的時候,作業系統切換線程內容相關的效能消耗是不能忽略的,在構建高效能web之路------web伺服器長串連
可以看出在進程切換上的代價,當然線程會更輕量一些,不過道理是類似的
記憶體同步
當使用到synchronized、volatile或Lock的時候,都會為了保證可見度導致更多的記憶體同步,這就無法享受到JMM結構帶來了效能最佳化。