除了保證操作的原子性以外盹牧,同步還可以保證變量在不同線程之間的內(nèi)存可見性通危。原子性和可見性共同構(gòu)成了同步的兩個核心要素熄守。第三章主要講述如何在線程之間安全的發(fā)布和共享變量蜈垮。
首先可以通過書上的例子來看一下什么是可見性。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
表面上我們啟動了兩個線程裕照,一個線程對ready flag進(jìn)行無限期的檢查攒发,而另一個線程則對number和ready進(jìn)行改變。從邏輯上說晋南,我們可能會期待屏幕上打印出42.但是實(shí)際上由于可見性問題晨继,另一個線程可能永遠(yuǎn)也看不到ready的值,因?yàn)镴MM中線程都擁有自己的工作內(nèi)存搬俊,并且只能對自己的工作內(nèi)存進(jìn)行存取紊扬,工作內(nèi)存中保存的其實(shí)是主內(nèi)存的一個副本。也有可能JVM會對指令進(jìn)行重排序優(yōu)化唉擂,盡管在單線程下會保證得到的最終結(jié)果是我們期待的邏輯結(jié)果(as-if-serial)餐屎,但是其指令的執(zhí)行過程不一定是聲明的順序。所以上面有可能是先對ready進(jìn)行了置位玩祟,導(dǎo)致之后打印出了零腹缩。在多線程中如果沒有進(jìn)行同步或者volatile聲明(在缺乏同步的程序中),就不能對指令的執(zhí)行順序抱有期待空扎。
可見性問題會導(dǎo)致一個問題那就是其他線程得到的數(shù)據(jù)可能是已經(jīng)過期的藏鹊,但其他線程卻一無所知。所以在可見性上java提供了volatile關(guān)鍵字來進(jìn)行可見性的保證转锈。如果將一個變量聲明為volatile盘寡,jvm會在寫入時將線程工作內(nèi)存的最新值拷貝回主內(nèi)存保證值是最新的,以及每次使用時都要刷新主內(nèi)存的值撮慨,并且所有與volatile變量相關(guān)的語句都將禁止重排序竿痰。volatile雖然可以保證可見性,卻不能保證操作的原子性砌溺。
總的來說影涉。volatile作為更加輕量化的線程安全手段,適用的范圍比同步更加有限规伐。書上說:
1.寫入的操作不應(yīng)該依賴于當(dāng)前的值蟹倾,因?yàn)関olatile無法保證原子性。
2.該變量沒有被納入其他變量的不變式關(guān)系之中猖闪,也就是說他是獨(dú)立的鲜棠。
3.訪問時不需要加鎖。
只有都滿足上面三個條件時萧朝,volatile才適用岔留。
發(fā)布指的是發(fā)布一個對象(實(shí)質(zhì)上是發(fā)布對對象的引用)使得對象可以再當(dāng)前作用域之外使用。發(fā)布是會破壞封裝性的检柬。當(dāng)一個不應(yīng)發(fā)布的對象被發(fā)布時就產(chǎn)生了溢出問題献联。發(fā)布一個對象到外部的最簡單的方法是使用一個公有的static變量來保存發(fā)布對象。
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<Secret>();
}
通過一個公有的靜態(tài)變量指向我們新建的HashSet何址,其他線程得到了新建HashSet的引用里逆。
在發(fā)布對象時,可能會間接地發(fā)布其他對象用爪。例如我發(fā)布一個hashset原押,那么hashset里面的值便也被間接的發(fā)布了。
有一種逸出是對于可變的private對象偎血,private訪問權(quán)限將這個對象封裝到當(dāng)前類诸衔,如果將其發(fā)布到外部便使得其他線程得到了關(guān)于一個我們希望是pirvate對象的引用盯漂,這違背了使用private原有的語義。
class UnsafeStates {
private String[] states = new String[] {
"AK", "AL" ...
};
public String[] getStates() { return states; }
}
另一種比較隱晦的發(fā)布其他對象的例子是內(nèi)部類笨农,由于內(nèi)部類中保存著對于outerclass的外部應(yīng)用就缆,當(dāng)我發(fā)布一個innnerclass時會間接的發(fā)布外部類的引用,可能會造成意想不到的結(jié)果谒亦。
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
最后是動態(tài)的construct初始化過程竭宰,如果我們在constructor的構(gòu)造過程中向外發(fā)布this引用,那么發(fā)布出去的對象很有可能是部分構(gòu)造的份招,即使是發(fā)布代碼是最后一行切揭,由于重排序也有可能是部分構(gòu)造的。所以對象的this引用只有在完成constructor的構(gòu)造之后才可以發(fā)布锁摔。
一個比較常見的錯誤是在構(gòu)造體內(nèi)啟動一個線程廓旬,由于線程是共享this引用的,所以啟動的線程可以看到未完全構(gòu)造好的對象鄙漏。所以在線程的啟動方法一般是start嗤谚。
對于這種情況,作者的建議是如果要對外發(fā)布對象,那么使用靜態(tài)的工廠方法來完成這個操作怔蚌。
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;
}
}
還有其他技術(shù)可以完成線程安全巩步。例如線程封閉將對象封閉在一個線程中,這樣無需其他同步手段就是線程安全的桦踊。1.stack封閉.由于stack是線程私有的椅野,被封閉在stack內(nèi)的對象自然只有當(dāng)前線程可以訪問。2.使用ThreadLocal類籍胯。3.在線程內(nèi)發(fā)布不可變對象竟闪,他們一定是線程安全的。滿足下列三條的是不可變對象:1杖狼。對象創(chuàng)建之后就不可變炼蛤。2.域都是final類型3.在對象構(gòu)造的過程中其this引用沒有逸出。
例如這種發(fā)布將public引用發(fā)布蝶涩,由于不可見性理朋,其他線程不一定可以看到最新的對象。靜態(tài)初始化和動態(tài)初始化不同绿聘,靜態(tài)初始化是JVM在類的load階段初始化的嗽上,JVM可以保證內(nèi)部的同步機(jī)制不出錯。
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
但是我們可以知道的是熄攘,對于一個對象兽愤,即便將這個對象的引用發(fā)布到其他線程,它的狀態(tài)也不一定是對于其他線程可見的。因?yàn)槠渌€程只能通過私有的對象公有API來訪問對象浅萧。所以即便我們發(fā)布了一個對象到其他線程逐沙,那么通過同步API方法和同步發(fā)布過程(安全發(fā)布),同樣可以讓這個對象是線程安全的惯殊。~