相關(guān)文章: 多線程安全性:每個(gè)人都在談,但是不是每個(gè)人都談地清
并發(fā)的意義在于多線程協(xié)作完成某項(xiàng)任務(wù)涯穷,而線程的協(xié)作就不可避免地需要共享數(shù)據(jù)。今天我們就來討論下如何發(fā)布和共享類對(duì)象编饺,使其可以被多個(gè)線程安全地訪問呀酸。
之前,我們討論了同步操作在多線程安全中如何保證原子性该贾,其實(shí)關(guān)鍵字synchronized不光實(shí)現(xiàn)了原子性羔杨,還實(shí)現(xiàn)內(nèi)存可見性(Memory Visibility)。也就是在同步的過程中杨蛋,不僅要防止某個(gè)線程正在使用的狀態(tài)被另一個(gè)線程修改兜材,還要保證一個(gè)線程修改了對(duì)象狀態(tài)之后理澎,其他線程能獲得更新之后的狀態(tài)。
1. 內(nèi)存可見性
在單個(gè)線程環(huán)境中曙寡,對(duì)某個(gè)變量寫入值后糠爬,在沒有其他寫操作的情況下,讀取該變量的值總是相同举庶;但是在多線程環(huán)境中情況并非如此执隧,雖然難以接受且違反直觀,但是很多問題就是這樣發(fā)生的灯变,這都是由于沒有使用同步機(jī)制保證可見性殴玛。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
//內(nèi)部靜態(tài)類可以直接使用外部類的靜態(tài)域
while (!ready){
// 線程讓步,使當(dāng)前線程從執(zhí)行狀態(tài)(運(yùn)行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))。
// 就是說當(dāng)一個(gè)線程使用了這個(gè)方法之后添祸,它就會(huì)把自己CPU執(zhí)行的時(shí)間讓掉滚粟,
// 讓自己或者其它的線程運(yùn)行。
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
//JVM可能對(duì)一些語句進(jìn)行重排序
number = 42;
ready = true;
}
}
上面的期望的代碼結(jié)果是:因主線程執(zhí)行ready = true
刃泌,匿名子線程退出循環(huán)凡壤,打印number。但是很可能事與愿違:由于匿名線程和主線程并不是一個(gè)線程環(huán)境耙替,雖然主線程中更新了ready變量的值亚侠,但是由于缺少同步機(jī)制,更新之后的值不一定對(duì)匿名子線程是可見的俗扇,匿名子線程很可能就由于使用了失效的數(shù)據(jù)而不能正常工作.
失效數(shù)據(jù)是由于Java的內(nèi)存機(jī)制導(dǎo)致的:在沒有同步機(jī)制的情況下硝烂,在多線程的環(huán)境中,每個(gè)進(jìn)程單獨(dú)使用保存在自己的線程環(huán)境中的變量拷貝铜幽。正因如此滞谢,當(dāng)多線程共享一個(gè)可變狀態(tài)時(shí),該狀態(tài)就會(huì)有多份拷貝除抛,當(dāng)一個(gè)線程環(huán)境中的變量拷貝被修改了狮杨,并不會(huì)立刻就去更新其他線程中的變量拷貝。
有些情況下到忽,上面的程序會(huì)輸出0橄教,這是由于重排序的發(fā)生,也就是JVM根據(jù)優(yōu)化的需要調(diào)整“不相關(guān)”代碼的執(zhí)行順序喘漏。在主線程中护蝶,number = 42
和ready = true
看似是不相關(guān)的,不相互依賴翩迈,所以可能被JVM在編譯時(shí)顛倒執(zhí)行順序滓走,所以才會(huì)出現(xiàn)這個(gè)奇怪結(jié)果。
重排序和變量多拷貝可能看上去是一種奇怪的設(shè)計(jì)帽馋,但是這樣做的目的是希望JVM能充分利用多核處理器強(qiáng)大的性能搅方,Java內(nèi)存模型更為具體的內(nèi)容將會(huì)在未來的篇章中為大家詳細(xì)介紹。
1.1 加鎖和可見性
正像前文提到同步控制那樣绽族,加鎖的含義也不僅僅局限于建立互斥性以保證原子性姨涡,還涉及到內(nèi)存可見性。為確保所有線程都能看到共享變量的最新值吧慢,所有對(duì)該變量執(zhí)行讀操作和寫操作的線程都必須在同一個(gè)鎖上同步涛漂。
1.2 Volatile變量
加鎖當(dāng)然是多線程安全的完備方法,但是有的時(shí)候只需要確保少數(shù)狀態(tài)變量的可見性即可检诗,使用加鎖機(jī)制未免有些大材小用匈仗,因此Java語言提供一種稍弱的同步機(jī)制——Volatile變量。當(dāng)變量被聲明為Volatile類型后逢慌,在編譯時(shí)和運(yùn)行時(shí)悠轩,JVM都會(huì)注意到這是一個(gè)共享變量,既不會(huì)在編譯時(shí)對(duì)該變量的操作進(jìn)行重排序攻泼,也不會(huì)緩存該變量到其他線程不可見的地方火架,保證所有線程都能讀取到該變量的最新狀態(tài)。
訪問Volatile變量時(shí)并沒使用加鎖操作忙菠,不會(huì)阻塞線程的運(yùn)行何鸡,所以性能遠(yuǎn)遠(yuǎn)優(yōu)于同步代碼塊和上鎖機(jī)制,只比訪問正常變量略高牛欢,不過這是犧牲原子性為代價(jià)的骡男。
加鎖機(jī)制可以確保可見性傍睹、原子性和不可重排序性隔盛,但是Volatile變量只能確保可見性和不可重排序性。
使用Volatile變量時(shí)需要謹(jǐn)慎焰望,一定要確保以下所有條件:
- 對(duì)當(dāng)前變量的寫操作骚亿,不依賴變量的當(dāng)前值(比如++操作就不符合要求),或者確保只有一個(gè)進(jìn)程更新該變量狀態(tài)熊赖;
- 該變量不會(huì)和其他變量一起納入不變性條件中来屠;
- 訪問該變量不需要加鎖;
實(shí)際使用中震鹉,Volatile變量多使用在會(huì)發(fā)生狀態(tài)翻轉(zhuǎn)的標(biāo)志位上俱笛。
2. 發(fā)布與逸出
對(duì)象的可見性是保證對(duì)象的最新狀態(tài)被共享,同時(shí)我們還應(yīng)該注意防止不應(yīng)該被共享的對(duì)象被暴露在多線程環(huán)境中传趾。
發(fā)布對(duì)象意味著該對(duì)象能在當(dāng)前作用域之外的代碼中被使用迎膜,比如,將類內(nèi)部的對(duì)象傳給其他類使用浆兰,或者一個(gè)非私有方法返回了該對(duì)象的引用等等磕仅。Java中強(qiáng)調(diào)類的封裝性就是希望能合理的發(fā)布對(duì)象珊豹,保護(hù)類的內(nèi)部信息。發(fā)布類內(nèi)部狀態(tài)榕订,在多線程的環(huán)境下可能問題不大店茶,但是在并發(fā)環(huán)境中卻用可能嚴(yán)重地破壞多線程安全。
某個(gè)不該發(fā)布的對(duì)象被發(fā)布了劫恒,這種情況被稱為逸出.
我們來一起看看幾種逸出的例子:
class UnsafeStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};
public String[] getStates() {
return states;
}
}
上面的例子中贩幻,雖然states
是私有變量,但是其被共有方法所暴露两嘴,數(shù)組中的元素都可以被任意修改丛楚,這就是一種逸出的情況。
當(dāng)一個(gè)對(duì)象被發(fā)布時(shí)憔辫,該對(duì)象的非私有域中的所有引用都會(huì)被發(fā)布趣些,即間接發(fā)布。
有一種逸出是比較隱蔽的螺垢,就是This逸出:
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
內(nèi)部的匿名類是隱私持有外部類的this引用的喧务,這就無意中將this發(fā)布給內(nèi)部類,如果內(nèi)部類再被發(fā)布枉圃,則外部類就可能逸出功茴,無意間造成內(nèi)存泄漏和多線程安全問題。
具體來說孽亲,只有當(dāng)構(gòu)造器執(zhí)行結(jié)束后坎穿,this對(duì)象完成初始化后才能發(fā)布,否者就是一種不正確的構(gòu)造返劲,存在多線程安全隱患玲昧。
解決這個(gè)問題最常見的方法就是工廠模式:
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
上例中,外部類的構(gòu)造器被設(shè)置為私有的篮绿,其他類執(zhí)行外部類的公有靜態(tài)方法在構(gòu)造器執(zhí)行完畢之后才返回對(duì)象的引用孵延,避免了this對(duì)象的逸出問題。
相對(duì)而言亲配,對(duì)象安全發(fā)布的問題比可見性問題更容易被忽視尘应,接下來就討論下如何才能安全發(fā)布對(duì)象。
3. 線程封閉
對(duì)象的發(fā)布既然是個(gè)頭疼的問題吼虎,所以我們應(yīng)該避免泛濫地發(fā)布對(duì)象犬钢,最簡(jiǎn)單的方式就是盡可能把對(duì)象的使用范圍都控制在單線程環(huán)境中,也就是線程封閉思灰。
常見的線程封閉方法有:
- Ad-hoc線程封閉玷犹,也就是維護(hù)線程封閉性的責(zé)任完全由編程承擔(dān),這種方法是不推薦的洒疚;
- 局部變量封閉歹颓,很多人容易忽視一點(diǎn)坯屿,局部變量的固有屬性之一就是封閉在執(zhí)行線程內(nèi),無法被外界引用晴股,所以盡量使用局部變量可以減少逸出的發(fā)生愿伴;
- ThreadLocal,這是一種更為規(guī)范的方法电湘,該類將把進(jìn)程中的某個(gè)值和保存值的對(duì)象關(guān)聯(lián)起來,并提供get和set方法鹅经,保證get方法獲得的值都是當(dāng)前進(jìn)程調(diào)用set方法設(shè)置的最新值寂呛。
需要說明的是,看起來是ThreadLocal類似于一種 Map<Thread, T>對(duì)象瘾晃,來保存特定于線程的值贷痪,但實(shí)際上這些值** **,其生命周期和Thread對(duì)象一致蹦误,一旦線程終止后劫拢,線程對(duì)象中的值都會(huì)被回收。
ThreadLoacl在JDBC和J2EE容器中有著大量的應(yīng)用强胰。比如舱沧,在JDBC中,ThreadLoacl用來保證每個(gè)線程只能有一個(gè)數(shù)據(jù)庫(kù)連接偶洋,再如在J2EE中熟吏,用以保存線程的上下文,方便線程切換等玄窝。
4. 不變性
如果一定要將發(fā)布對(duì)象牵寺,那么不可變的對(duì)象是首選,因?yàn)槠湟欢ㄊ嵌嗑€程安全的恩脂,可以放心地被用來數(shù)據(jù)共享帽氓。這是因?yàn)椴蛔兊膶?duì)象的狀態(tài)只有一種狀態(tài),并且該狀態(tài)由其構(gòu)造器控制俩块。
對(duì)象不可變要求滿足以下條件:
- 該對(duì)象是正確創(chuàng)建的黎休,沒有this逸出問題;
- 該對(duì)象的所有狀態(tài)在創(chuàng)建之后不能修改典阵,也就是其set方法應(yīng)該為私有的奋渔,或者該域直接是final的。
下面這個(gè)類就是不可變的:
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
public String getStoogeNames() {
List<String> stooges = new Vector<String>();
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
return stooges.toString();
}
}
《Effective Java》建議在類設(shè)計(jì)時(shí)應(yīng)該盡可能減少可變的域:除非必須壮啊,域都應(yīng)該是私有域嫉鲸;除非可變,域都應(yīng)該是final域歹啼。
5. 安全發(fā)布
要安全地發(fā)布一個(gè)對(duì)象玄渗,對(duì)象的引用以及對(duì)象的狀態(tài)必須同時(shí)對(duì)其他線程可見座菠。一個(gè)正確構(gòu)造的對(duì)象可以通過以下方式安全地發(fā)布:
- 在靜態(tài)初始化函數(shù)中初始化一個(gè)對(duì)象的引用(態(tài)初始化函數(shù)由JVM在初始化階段執(zhí)行,JVM為其提供同步機(jī)制)藤树;
- 將對(duì)象的引用保存在Volatile域或AtomicReference對(duì)象中浴滴;
- 將對(duì)象的引用保存在某個(gè)正確構(gòu)造對(duì)象的final域中;
- 將對(duì)象的引用保存到一個(gè)由鎖保護(hù)的域中岁钓;
- 將對(duì)象的引用保存到線程安全容器中升略;
6. 總結(jié)
在討論過可見性和安全發(fā)布之后,我們來總結(jié)下安全共享對(duì)象的策略:
- 線程封閉:線程封閉的對(duì)象只能由一個(gè)線程擁有屡限,對(duì)象封閉在線程中品嚣,并且只能由該線程修改。
- 只讀共享:共享不可變的只讀對(duì)象钧大,只要保證可見性即可翰撑,可以不需要額外的同步操作。
- 線程安全共享:線程安全的對(duì)象在其內(nèi)部封裝同步機(jī)制啊央,多線程通過公有接口訪問數(shù)據(jù)眶诈;對(duì)象發(fā)布的內(nèi)部狀態(tài)必須是安全發(fā)布的,且可變的狀態(tài)需要鎖來保護(hù)瓜饥;對(duì)象的引用和對(duì)象的狀態(tài)都是可見的逝撬。
后續(xù)預(yù)告:Java內(nèi)存模型