除了保證操作的原子性以外,同步還可以保證變量在不同線程之間的內存可見性。原子性和可見性共同構成了同步的兩個核心要素。第三章主要講述如何在線程之間安全的發布和共享變量。
首先可以通過書上的例子來看一下什么是可見性。
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進行無限期的檢查,而另一個線程則對number和ready進行改變。從邏輯上說,我們可能會期待屏幕上打印出42.但是實際上由于可見性問題,另一個線程可能永遠也看不到ready的值,因為JMM中線程都擁有自己的工作內存,并且只能對自己的工作內存進行存取,工作內存中保存的其實是主內存的一個副本。也有可能JVM會對指令進行重排序優化,盡管在單線程下會保證得到的最終結果是我們期待的邏輯結果(as-if-serial),但是其指令的執行過程不一定是聲明的順序。所以上面有可能是先對ready進行了置位,導致之后打印出了零。在多線程中如果沒有進行同步或者volatile聲明(在缺乏同步的程序中),就不能對指令的執行順序抱有期待。
可見性問題會導致一個問題那就是其他線程得到的數據可能是已經過期的,但其他線程卻一無所知。所以在可見性上java提供了volatile關鍵字來進行可見性的保證。如果將一個變量聲明為volatile,jvm會在寫入時將線程工作內存的最新值拷貝回主內存保證值是最新的,以及每次使用時都要刷新主內存的值,并且所有與volatile變量相關的語句都將禁止重排序。volatile雖然可以保證可見性,卻不能保證操作的原子性。
總的來說。volatile作為更加輕量化的線程安全手段,適用的范圍比同步更加有限。書上說:
1.寫入的操作不應該依賴于當前的值,因為volatile無法保證原子性。
2.該變量沒有被納入其他變量的不變式關系之中,也就是說他是獨立的。
3.訪問時不需要加鎖。
只有都滿足上面三個條件時,volatile才適用。
發布指的是發布一個對象(實質上是發布對對象的引用)使得對象可以再當前作用域之外使用。發布是會破壞封裝性的。當一個不應發布的對象被發布時就產生了溢出問題。發布一個對象到外部的最簡單的方法是使用一個公有的static變量來保存發布對象。
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<Secret>();
}
通過一個公有的靜態變量指向我們新建的HashSet,其他線程得到了新建HashSet的引用。
在發布對象時,可能會間接地發布其他對象。例如我發布一個hashset,那么hashset里面的值便也被間接的發布了。
有一種逸出是對于可變的private對象,private訪問權限將這個對象封裝到當前類,如果將其發布到外部便使得其他線程得到了關于一個我們希望是pirvate對象的引用,這違背了使用private原有的語義。
class UnsafeStates {
private String[] states = new String[] {
"AK", "AL" ...
};
public String[] getStates() { return states; }
}
另一種比較隱晦的發布其他對象的例子是內部類,由于內部類中保存著對于outerclass的外部應用,當我發布一個innnerclass時會間接的發布外部類的引用,可能會造成意想不到的結果。
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
最后是動態的construct初始化過程,如果我們在constructor的構造過程中向外發布this引用,那么發布出去的對象很有可能是部分構造的,即使是發布代碼是最后一行,由于重排序也有可能是部分構造的。所以對象的this引用只有在完成constructor的構造之后才可以發布。
一個比較常見的錯誤是在構造體內啟動一個線程,由于線程是共享this引用的,所以啟動的線程可以看到未完全構造好的對象。所以在線程的啟動方法一般是start。
對于這種情況,作者的建議是如果要對外發布對象,那么使用靜態的工廠方法來完成這個操作。
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;
}
}
還有其他技術可以完成線程安全。例如線程封閉將對象封閉在一個線程中,這樣無需其他同步手段就是線程安全的。1.stack封閉.由于stack是線程私有的,被封閉在stack內的對象自然只有當前線程可以訪問。2.使用ThreadLocal類。3.在線程內發布不可變對象,他們一定是線程安全的。滿足下列三條的是不可變對象:1。對象創建之后就不可變。2.域都是final類型3.在對象構造的過程中其this引用沒有逸出。
例如這種發布將public引用發布,由于不可見性,其他線程不一定可以看到最新的對象。靜態初始化和動態初始化不同,靜態初始化是JVM在類的load階段初始化的,JVM可以保證內部的同步機制不出錯。
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
但是我們可以知道的是,對于一個對象,即便將這個對象的引用發布到其他線程,它的狀態也不一定是對于其他線程可見的。因為其他線程只能通過私有的對象公有API來訪問對象。所以即便我們發布了一個對象到其他線程,那么通過同步API方法和同步發布過程(安全發布),同樣可以讓這個對象是線程安全的。~