常見的并發陷阱
volatile
volatile只能強調數據的可見性,并不能保證原子操作和線程安全,因此volatile不是萬能的。參考指令重排序
volatile最常見于下面兩種場景。
a. 循環檢測機制
volatile boolean done =
false;
while( ! done ){
dosomething();
}
b. 單例模型 (http://www.tkk7.com/xylz/archive/2009/12/18/306622.html)
synchronized/Lock
看起來Lock有更好的性能以及更靈活的控制,是否完全可以替換synchronized?
在鎖的一些其它問題中說過,synchronized的性能隨著JDK版本的升級會越來越高,而Lock優化的空間受限于CPU的性能,很有限。另外JDK內部的工具(線程轉儲)對synchronized是有一些支持的(方便發現死鎖等),而對Lock是沒有任何支持的。
也就說簡單的邏輯使用synchronized完全沒有問題,隨著機器的性能的提高,這點開銷是可以忽略的。而且從代碼結構上講是更簡單的。簡單就是美。
對于復雜的邏輯,如果涉及到讀寫鎖、條件變量、更高的吞吐量以及更靈活、動態的用法,那么就可以考慮使用Lock。當然這里尤其需要注意Lock的正確用法。
Lock lock =

lock.lock();
try{
//do something
}
finally{
lock.unlock();
}
一定要將Lock的釋放放入finally塊中,否則一旦發生異常或者邏輯跳轉,很有可能會導致鎖沒有釋放,從而發生死鎖。而且這種死鎖是難以排查的。
如果需要synchronized無法做到的嘗試鎖機制,或者說擔心發生死鎖無法自恢復,那么使用tryLock()是一個比較明智的選擇的。
Lock lock =
if(lock.tryLock()){
try{
//do something
}
finally{
lock.unlock();
}
}
甚至可以使用獲取鎖一段時間內超時的機制Lock.tryLock(long,TimeUnit)。 鎖的使用可以參考前面文章的描述和建議。
鎖的邊界
一個流行的錯誤是這樣的。
ConcurrentMap<String,String> map = new ConcurrentHashMap<String,String>();
if(!map.containsKey(key)){
map.put(key,value);
}
看起來很合理的,對于一個線程安全的Map實現,要存取一個不重復的結果,先檢測是否存在然后加入。 其實我們知道兩個原子操作和在一起的指令序列不代表就是線程安全的。 割裂的多個原子操作放在一起在多線程的情況下就有可能發生錯誤。
實際上ConcurrentMap提供了putIfAbsent(K, V)的“原子操作”機制,這等價于下面的邏輯:
if(map.containsKey(key)){
return map.get(key);
}else{
return map.put(k,v);
}
除了putIfAbsent還有replace(K, V)以及replace(K, V, V)兩種機制來完成組合的操作。
提到Map,這里有一篇談HashMap讀寫并發的問題。
構造函數啟動線程
下面的實例是在構造函數中啟動一個線程。
public class Runner{
int x,y;
Thread thread;
public Runner(){
this.x=1;
this.y=2;
this.thread=new MyThread();
this.thread.start();
}
}
這里可能存在的陷阱是如果此類被繼承,那么啟動的線程可能無法正確讀取子類的初始化操作。
因此一個簡單的原則是,禁止在構造函數中啟動線程,可以考慮但是提供一個方法來啟動線程。如果非要這么做,最好將類設置為final,禁止繼承。
丟失通知的問題
這篇文章里面提到過notify丟失通知的問題。
對于wait/notify/notifyAll以及await/singal/singalAll,如果不確定到底是否能夠正確的收到消息,擔心丟失通知,簡單一點就是總是通知所有。
如果擔心只收到一次消息,使用循環一直監聽是不錯的選擇。
非常主用性能的系統,可能就需要區分到底是通知單個還是通知所有的掛起者。
線程數
并不是線程數越多越好,在下一篇文章里面會具體了解下性能和可伸縮性。 簡單的說,線程數多少沒有一個固定的結論,受限于CPU的內核數,IO的性能以及依賴的服務等等。因此選擇一個合適的線程數有助于提高吞吐量。
對于CPU密集型應用,線程數和CPU的內核數一致有助于提高吞吐量,所有CPU都很繁忙,效率就很高。 對于IO密集型應用,線程數受限于IO的性能,某些時候單線程可能比多線程效率更高。但通常情況下適當提高線程數,有利于提高網絡IO的效率,因為我們總是認為網絡IO的效率比較低。
對于線程池而言,選擇合適的線程數以及任務隊列是提高線程池效率的手段。
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
對于線程池來說,如果任務總是有積壓,那么可以適當提高corePoolSize大小;如果機器負載較低,那么可以適當提高maximumPoolSize的大小;任務隊列不長的情況下減小keepAliveTime的時間有助于降低負載;另外任務隊列的長度以及任務隊列的拒絕策略也會對任務的處理有一些影響。
©2009-2014 IMXYLZ
|求賢若渴