1. 程式人生 > >謎題85:惰性初始化

謎題85:惰性初始化

The ini exceptio 直觀 分析 start exce cep thread

下面這個可憐的小類實在是太懶了,甚至於都不願意用通常的方法進行初始化,
所以它求助於後臺線程。這個程序會打印什麽呢?每次你運行它的時候都會打印
出相同的東西嗎?

public class Lazy {
private static boolean initialized = false;
static {
Thread t = new Thread(new Runnable() {
public void run() {
initialized = true;
}
});
t.start();
try{
t.join();
}catch (InterruptedException e){
throw new AssertionError(e);
}
}
public static void main(String[] args){
System.out.println(initialized);
}
}

  


雖然有點奇怪,但是這個程序看起來很直觀的。靜態域 initialized 初始時被設
為 false。然後主線程創建了一個後臺線程,該線程的 run 方法將 initialized

的值設為 true。主線程啟動了後臺線程之後,就調用了 join 方法等待它的結束。
當後臺線程完成運行的時候,毫無疑問 initialized 的值已經被設為了 true。
當且僅當這個時候,調用了 main 方法的主線程會打印出 initialized 的值。如
果是這樣的話,程序肯定會打印出 true 嗎?如果你運行該程序,你會發現它不
會打印任何東西,它只是被掛起了。
為了理解這個程序的行為,我們需要模擬它初始化的細節。當一個線程訪問一個

類的某個成員的時候,它會去檢查這個類是否已經被初始化。在忽略嚴重錯誤的
情況下,有 4 種可能的情況[JLS 12.4.2]:
• 這個類尚未被初始化。
• 這個類正在被當前線程初始化:這是對初始化的遞歸請求。
• 這個類正在被其他線程而不是當前線程初始化。
• 這個類已經被初始化。
當主線程調用 Lazy.main 方法時,它會檢查 Lazy 類是否已經被初始化。此時它
並沒有被初始化(情況 1),所以主線程會記錄下當前正在進行初始化,並開始對
這個類進行初始化。按照我們前面的分析,主線程會將 initialized 的值設為
false,創建並啟動一個後臺線程,該線程的 run 方法會將 initialized 設為
true,然後主線程會等待後臺線程執行完畢。此時,有趣的事情開始了。
那個後臺線程調用了它的 run 方法。在該線程將 Lazy.initialized 設為 true
之前,它也會去檢查 Lazy 類是否已經被初始化。這個時候,這個類正在被另外
一個線程進行初始化(情況 3)。在這種情況下,當前線程,也就是那個後臺線
程,會等待 Class 對象直到初始化完成。遺憾的是,那個正在進行初始化工作的
線程,也就是主線程,正在等待著後臺線程運行結束。因為這 2 個線程現在正相
互等待著,該程序就死鎖了(deadlock)。這就是所有的一切,真是遺憾。有 2
種方法可以訂正這個程序。到目前為止,最好的方法就是不要在類進行初始化的
時候啟動任何後臺線程:有些時候,2 個線程並不比 1 個線程好。更一般的講,
要讓類的初始化盡可能地簡單。訂正這個程序的第 2 種方法就是讓主線程在等待
後臺線程之前就完成類的初始化:

// Bad way to eliminate the deadlock. Complex and error prone
public class Lazy {
private static boolean initialized = false;
private static Thread t = new Thread(new Runnable() {
public void run() {
initialized = true;
}
});
static {
t.start();
}
public static void main(String[] args){

try{
t.join();
}catch (InterruptedException e){
throw new AssertionError(e);
}
System.out.println(initialized);
}
}

  

雖然這麽做確實消除了死鎖,但是它卻是一個非常不好的想法。主線程需要等待
後臺線程完成工作,但是其他的線程不需要這麽做。一旦主線程完成了對 Lazy
類的初始化,其他線程就可以使用這個類了。這使得在 initialized 的值還是
false 的時候,其他線程就可以觀察到它。
總之,在類的初始化期間等待某個後臺線程很可能會造成死鎖。要讓類初始化的
動作序列盡可能地簡單。類的自動初始化被公認為是語言設計上的難題,Java
的設計者們在這個方面做得很不錯。如果你寫了一些復雜的類初始化代碼,很多
種情況下,你這是在搬起石頭砸自己的腳。

謎題85:惰性初始化