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

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