測(cè)試和調(diào)試多線程程序是非常困難的,因?yàn)椴l(fā)危險(xiǎn)常常不會(huì)穩(wěn)定的發(fā)生。多數(shù)線程問(wèn)題都是不可預(yù)測(cè)的,可能不會(huì)發(fā)生在所有特定的平臺(tái)(如單處理器系統(tǒng))或低于某個(gè)特定水平荷載。因?yàn)闇y(cè)試多線程的正確性是很困難的秕硝,而且bug可能會(huì)花很長(zhǎng)時(shí)間才能重現(xiàn),所以洲尊,當(dāng)我們開(kāi)發(fā)程序是远豺,要從一開(kāi)始就考慮線程安全問(wèn)題。
危險(xiǎn)的條件
大多數(shù)并發(fā)危害歸結(jié)為某種數(shù)據(jù)競(jìng)爭(zhēng)坞嘀。數(shù)據(jù)競(jìng)爭(zhēng)或競(jìng)爭(zhēng)條件躯护,當(dāng)多個(gè)線程或進(jìn)程讀寫(xiě)共享數(shù)據(jù)項(xiàng),最終結(jié)果取決于線程的調(diào)度的順序。清單1給出一個(gè)示例的一個(gè)簡(jiǎn)單的數(shù)據(jù)競(jìng)爭(zhēng)實(shí)例丽涩,根據(jù)調(diào)度的線程可以打印0或1棺滞。
public class DataRace {
static int a = 0;
public static void main() {
new MyThread().start();
a = 1;
}
public static class MyThread extends Thread {
public void run() {
System.out.println(a);
}
}
}
第二個(gè)線程可能被馬上調(diào)度裁蚁,打印初始a的初始值0。此外继准,第二個(gè)線程可能不會(huì)立即執(zhí)行枉证,導(dǎo)致打印1。這個(gè)程序的輸出結(jié)果依賴你使用的JDK移必,操作系統(tǒng)的調(diào)度室谚。多運(yùn)行幾次會(huì)得到不同的結(jié)果。
可見(jiàn)性危害
在清代1 事實(shí)上還有另一個(gè)數(shù)據(jù)沖突崔泵,除了在第二個(gè)線程執(zhí)行之前秒赤,第一個(gè)線程把a(bǔ)設(shè)置為1。第二個(gè)沖突時(shí)可見(jiàn)性沖突:兩個(gè)線程沒(méi)有使用 synchronization憎瘸,如果第二個(gè)線程在第一個(gè)線程賦值a之后執(zhí)行入篮,第一個(gè)線程的賦值可能或不可能立刻對(duì)第二個(gè)線程可見(jiàn)。第二個(gè)線程可能看到的a還是0幌甘,計(jì)時(shí)線程1已經(jīng)設(shè)置成了1,潮售。
這二類數(shù)據(jù)沖突,兩個(gè)線程在沒(méi)有適當(dāng)?shù)耐皆L問(wèn)相同的變量锅风,是一個(gè)復(fù)雜的問(wèn)題饲做,但幸運(yùn)的是,你可以避免這類沖突利用同步遏弱,當(dāng)你閱讀一個(gè)變量,可能是由另一個(gè)線程寫(xiě)入的數(shù)據(jù)塞弊,或者寫(xiě)一個(gè)變量就被由另一個(gè)線程讀取漱逸。我們不會(huì)對(duì)這類數(shù)據(jù)競(jìng)爭(zhēng)在這里進(jìn)一步探索,請(qǐng)看 "Synching up with the Java Memory Model" 側(cè)邊欄和相關(guān)課題組對(duì)這一復(fù)雜問(wèn)題的更多信息游沿。
在構(gòu)造方法中不要公布“this”指針
將數(shù)據(jù)競(jìng)爭(zhēng)引入到類中的一個(gè)錯(cuò)誤是在構(gòu)造函數(shù)完成之前將this引用暴露給另一個(gè)線程饰抒。有時(shí)引用是顯式的,例如直接將其存儲(chǔ)在靜態(tài)字段或集合中诀黍,但其他時(shí)間可以是隱式的袋坑,例如在構(gòu)造函數(shù)中向非靜態(tài)內(nèi)部類的實(shí)例發(fā)布引用時(shí)。構(gòu)造函數(shù)不是普通的方法眯勾,它們具有初始化安全的特殊語(yǔ)義枣宫。在構(gòu)造函數(shù)完成后,對(duì)象被假定為可預(yù)測(cè)的吃环、一致的狀態(tài)也颤,并且對(duì)未完全構(gòu)造對(duì)象的引用是危險(xiǎn)的。清單2展示了將這種競(jìng)爭(zhēng)條件引入構(gòu)造函數(shù)的示例郁轻。它看起來(lái)無(wú)害翅娶,但它包含了嚴(yán)重并發(fā)問(wèn)題的種子文留。
public class EventListener {
public EventListener(EventSource eventSource) {
// do our initialization
...
// register ourselves with the event source
eventSource.registerListener(this);
}
public onEvent(Event e) {
// handle the event
}
}
初次檢查,事件偵聽(tīng)器類看起來(lái)是無(wú)害的竭沫。偵聽(tīng)器的注冊(cè)是一個(gè)新的對(duì)象燥翅,而另一個(gè)線程可能看到它,這是構(gòu)造函數(shù)所做的最后一件事蜕提。但即使忽略所有的java內(nèi)存模型(JMM)如在線程和內(nèi)存訪問(wèn)排序能見(jiàn)度差異問(wèn)題森书,這個(gè)代碼仍然是不完全暴露在了危險(xiǎn)事件偵聽(tīng)器對(duì)象的其他線程」峤Γ考慮會(huì)發(fā)生什么時(shí)拄氯,EventListener是它的子類,如清單3所示:
public class RecordingEventListener extends EventListener {
private final ArrayList list;
public RecordingEventListener(EventSource eventSource) {
super(eventSource);
list = Collections.synchronizedList(new ArrayList());
}
public onEvent(Event e) {
list.add(e);
super.onEvent(e);
}
public Event[] getEvents() {
return (Event[]) list.toArray(new Event[0]);
}
}
因?yàn)閖ava語(yǔ)言規(guī)范要求調(diào)用super()必須是子類構(gòu)造方法的第一行代碼它浅。在子類完成初始化之前译柏,我們尚未構(gòu)造完成的event listener 已經(jīng)注冊(cè)到了event source。現(xiàn)在我們的list存在數(shù)據(jù)競(jìng)爭(zhēng)姐霍。RecordingEventListener.onEvent()在調(diào)用時(shí)可能list還是默認(rèn)值null鄙麦,會(huì)拋出NullPointerException 異常。
參考
Safe construction techniques
JSR 133 (Java Memory Model) FAQ