Java並行:線程安全前傳之Singleton

1.寂寞的Singleton

    如果你是一名OO程序員,Singleton的名字對你來說就不會陌生,它是GoF設計模式的一種,江湖人稱“單例”的便是;即便你不是OO程序員,中國人你總該是吧?那麼下面一段你應該也會背:“世界上只有一個敏感詞,敏感詞是敏感詞的一部分,敏感詞是代表敏感詞的唯一合法敏感詞,任何企圖製造兩個敏感詞的企圖都是註定要失敗的。”說的多麼好!一語道破Singleton的真諦。但是,爲了讓帖子存活下去,爲了更好地娛樂大衆,下面我們從“敏感詞系統”轉到“世界盃系統”,我們來看:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class WorldCup {
	private static WorldCup instance = new WorldCup();
	
	public static WorldCup getInstance(){
		return instance;
	}
}

    這就是一個極爲簡易的Singleton範例,但是雷米特同學看到這個類估計哭得心思都有了:這裏的instance是eager initialization,也就是說作爲世界盃的發起人,雷米特小朋友必須在提出“世界盃”這個概念的時候,就自己掏錢鑄一座金盃撂那,這賽事成不成還兩說。擱誰誰也不樂意。那這事咋整?雷米特老婆溫柔地說,“你個完蛋敗家玩意,那就等破世界盃板上釘釘,第一屆舉辦的時候再造唄!”真是一語驚醒夢中人,雷米特立刻打開IDE,敲出下面的代碼:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class WorldCup {
	private static WorldCup instance;
	
	public static WorldCup getInstance(){
		if(instance == null) //A
			instance = new WorldCup(); //B
		return instance;
	}
}

    雷米特長出一口氣,這回lazy initialization,總高枕無憂了吧~

2. 當Singleton遇見多線程

    這時,溫柔賢惠的老婆又發話了:“你傻啊?倒黴玩意你想造多少個破杯啊?杯具啊,我~不~活~了~~我錯了,我從一開始就錯了,如果我不嫁過來……”雷米特表示理解不能,這麼NB的代碼錯在哪了?

    觀衆朋友們來分析一下,是什麼讓雷米特的老婆如此傷心欲絕呢?絕大部分朋友應該已經知道了,那就是多線程的問題。在A和B之間存在一個時間差,可能有t1,t2兩個線程,t1檢測instance爲null,沒有繼續執行B而是被切走,t2又檢測到instance爲null,這下,兩個世界盃就被造出來了。Singleton名不副實。那應該如何?你可能已經對我的傻逼描述煩不勝煩了,加鎖唄:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class WorldCup {
	private static WorldCup instance;

	public static WorldCup getInstance() {
		synchronized (WorldCup.class) {
			if (instance == null) // A
				instance = new WorldCup();// B
			return instance;
		}
	}
}

    問題解決,不是嗎?對,但不那麼完美。我們知道,加鎖/放鎖是很費的操作,這裏完全沒有必要每次調用getInstance都加鎖,事實上我們只想保證一次初始化成功而已,其餘的快速返回就好了。

3. 又見DCL

    那也不難,用傳說中的“雙檢鎖”(Double-Checked Lock)即可:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class WorldCup {
	private static WorldCup instance;

	public static WorldCup getInstance() {
		if (instance == null) { //C
			synchronized (WorldCup.class) {
				if (instance == null) // A
					instance = new WorldCup();// B
			}
		}
		return instance;
	}
}

   新加的C操作過濾掉了大量的“快速返回”,讓程序只有在真正需要加鎖時纔去加鎖,效率大漲。雷米特大喜過望,終於可以和老婆交差了。但是,結束了麼?

4.安全發佈

    “結束了麼?”一段時間之前的一次電話面試中,面對同樣的問題,面試官不懷好意地問。我立刻深深地覺得我被這兩個臭不要臉的傢伙徹底調戲了,萬分糾結地敗下陣來。相信雷米特再次面對他老婆時,也會有相同的感受。那麼,看似精巧的DCL,會有什麼問題呢?我們要從“安全發佈”談起。

    所謂對象的“發佈”(publish),是指使他能夠讓當前範圍以外的代碼使用。爲了方便理解,列舉幾種發佈對象的常用方法:

  • 把對象的引用存到公共靜態域裏。
  • 把一個對象傳遞給一個“外部方法”,所謂外部方法是指其他類的方法或者自身可以被子類重寫的方法(因爲你不知道這些方法會有些什麼動作)
  • 發佈一個Inner Class的實例。這是因爲每個Inner Class實例都保存了外部對象的引用。

    另外需要記住的規則是“當發佈一個對象時,實際上隱式地發佈了他的所有非私有域對象”。   

    發佈對象並不可怕,可怕的是錯誤地發佈對象。當一個對象還沒有做好準備時就將他發佈,我們稱作“逃逸”(escape)。舉一個簡單的例子:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class Argentina {
	private boolean isMessiReady;

	public Argentina() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				tell();  //Argentina.this
			}
		}).start();
		isMessiReady = true;
	}

	void tell() {
		System.out.println("Is Messi Here?:" + isMessiReady);
	}
}

    阿根廷隊隊伍還沒組建好就開新聞發佈會,這時Messi到底在不在呢?老馬可能在放煙霧彈。這裏的對象發佈屬於我們上面提到的第三種,即發佈內部類,因爲這裏的Thread其實是用一個匿名內部類Runnable實現的,新線程可以訪問到外部類,在我們加註釋的那一行其實隱含的訪問了外部類Argentina.this。這屬於臭名昭著的“this逃逸”,常見情景包括了“在構造函數中添加listener,啓動新線程或者調用可重寫方法”。

    其實說白了,所謂逃逸,無非是對象“發佈”和另一個線程訪問該對象之間沒有正確的Happens-before關係

    回過頭來看我們上面的DCL,他雖然不是“this逃逸”,但也屬於肇事逃逸的一種。一個線程t1的B操作和另一線程t2的C操作之間沒有HB關係,也就是對instance的讀寫沒有同步,可能會造成的現象是t1-B的new WorldCup()還沒有完全構造成功,但t2-C已經看到instance非空,這樣t2就直接返回了未完全構造的instance的引用,t2想當然地對instance進行操作,結果是微妙的。

    看到這裏,結合上一次的討論,可能你已經明白了,“說了這麼久,原來還不是勞什子的可見性問題,翠花,上volatile~”

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class WorldCup {
	private volatile static WorldCup instance;

	public static WorldCup getInstance() {
		if (instance == null) { //C
			synchronized (WorldCup.class) {
				if (instance == null) // A
					instance = new WorldCup();// B
			}
		}
		return instance;
	}
}

   “這下,你,們,該,滿,足,了,吧?”

5. Yet another 解決方法

    恭喜你,成功了。但是,其實我還想說,恭喜你,你out了。隨着時代的發展JVM的進步,DCL這樣的技巧已經逐步被淘汰了,而lazy initialization holder這樣的新秀在效率上和DCL已經沒什麼差別:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
public class WorldCup {
	private static class WorldCupHolder{
		public static WorldCup instance = new WorldCup();
	}

	public static WorldCup getInstance() {
		return WorldCupHolder.instance;
	}
}

    同樣是“惰性初始化”,這個是不是更好看?

    在這裏我們回過頭來看看我們最初的eager initialization,你這時可能會反過來思考,他不是volatile,會不會有escape問題?不會。因爲Java保證了域初始化段落對其餘操作的HB關係。好了,這下,雷米特家的河東獅估計可以休矣。

6. 討論的延續

    關於上面阿根廷的例子,寫的時候我發現一點疑問,把自己的理解拿出來和大家討論。那就是如果我把isMessiReady = true(記做A)放在新線程start(記做B)的前面,在這裏新線程不就可以保證HB關係了麼?因爲有IntraThread原則,A hb B,而我們又有start操作hb於任何該線程的動作,比如tell(記做C),那麼不就有A hb B hb C了麼?可以保證新聞發佈會上梅西肯定在場。那麼,爲什麼幾乎所有看到的資料裏都警告說“即使是構造函數的最後一個操作,也不要啓新線程、加listener、調可重寫函數”云云?我的理解是,是爲了防止這種情況:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/tomsheep/]
class SuperArgentina extends Argentina{
	private boolean isDiegoReady;
	public SuperArgentina() {
		super();
		isDiegoReady =true;
	}
	
	@Override
	protected void tell() {
		super.tell();
		System.out.println("Is Diego Here?:"+isDiegoReady);
	}
}

    我們拓展阿根廷爲“超級阿根廷”,加入老馬的狀態,這樣一來,老馬自己在不在場就成問題了。關於上面的這段分析,我的把握並不是特別大,希望大牛們能夠提點一下。

主要參考資料:

1. JavaWorld文章:Double-checked locking: Clever, but broken

2. Java Concurrency in Practice

3. GoF設計模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章