單例模式中的線程安全(延遲加載)

    設計模式中常用的單例模式,在jvm中可以保證該對象只有一個實例存在。這樣對於一些特別大的對象,可以很好的節省資源。由於省去了new,所以節省了gc消耗。同時,對於一些核心系統邏輯,可以能要一個對象實例來控制會省去很多麻煩。

     單例模式,如果不考慮多線程,則可以如下創建

public class Singleton {
	/* 持有私有靜態實例,防止被引用,此處賦值爲null,目的是實現延遲加載 */
	private static Singleton instance = null;
	/* 私有構造方法,防止被實例化 */
	private Singleton() {
	}
	/* 靜態工程方法,創建實例 */
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
	/* 如果該對象被用於序列化,可以保證對象在序列化前後保持一致 */
	public Object readResolve() {
		return instance;
	}
}

     上面提到延遲加載,所謂延遲加載,就是指當實際用到該對象時才加載對應類,否則只是聲明,並未實際花費資源去初始化對象。

      最好的理解方式,將上面代碼中如下語句,此時爲非延遲加載,加載該單例類時,也會初始化該靜態類的靜態成員變量,並調用new來創建實例。而採用延遲加載,則需要當調用getInstance()方法 時,纔會通過new初始化實例。

private static Singleton instance = new Singleton();

      以上是單例模式中延遲加載的解釋,但是上面的示例是不考慮多線程下的單例模式。如果多線程下進行延遲加載,上述單利模式是否有問題? 答案是有。

      如果多個線程同時調用getInstance,則可能會調用多次new,會產生問題。如何避免呢?我們很容易想到使用鎖、synchronized等方法。

      synchronized:

     如下,該方法可以保證類每次調用getInstance時,都只有一個線程在使用。但是問題也來了,每次調用都會鎖住這個對象,因爲synchronized用在方法上時,鎖住的是整個類對象(ps:如果一個對象有多個synchronized方法,某一時刻某個線程已經進入到了某個synchronized方法,那麼在該方法沒有執行完畢前,其他線程是無法訪問該對象的任何synchronized方法的。注意這時候是給對象上鎖,如果是不同的對象,則各個對象之間沒有限制關係。但如果是靜態方法的情況(方法加上static關鍵字),即便是向兩個線程傳入不同的對象(同一個類),這兩個線程仍然是互相制約的,必須先執行完一個,再執行下一個)我們不希望這樣子,因爲這樣子在併發處理中會損失性能。

public static synchronized Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}

     根據上面,我們做了改動,將synchronized關鍵字放在代碼塊中,這樣鎖定的就不是整個對象,而是方法塊,synchronized塊比synchronized方法更加細粒度地控制了多個線程的訪問,只有synchronized塊中的內容不能同時被多個線程所訪問,方法中的其他語句仍然可以同時被多個線程所訪問(包括synchronized塊之前的和之後的。如下

public static Singleton getInstance() {
		if (instance == null) {
			synchronized (instance) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}

       這個時候,貌似解決了問題。但是,我想說的是,從網上看到的資料裏面有如下問題情況:jvm優化使得new singleton()的操作實際上不是原子操作,包括分配內存和初始化對象兩個過程,很有可能第一個線程進入分配內存後,就已經將instance分配內存地址,此時不爲null,但是還沒有分對象初始化,此時另一個線程發現instance不等於null,就直接開始使用對象的其他方法,就會報錯,類似classnotdefine或者noclass之類的異常報錯。

       最後,不得不使用如下的代碼解決:

private static class SingletonFactory{         
        private static Singleton instance = new Singleton();         
    }         
    public static Singleton getInstance(){         
        return SingletonFactory.instance;         
    }

      以上爲SingletonFactory爲內部類,放在Singleton類中。JVM有個特性:一個類被加載時,該類是線程互斥的,且只會被加載一次。所以如上代碼,當調用getInstance()時(該方法可以多線程同時訪問),就可以實現加載類了(第一個線程訪問就會加載SingletonFactory,並創建Class對象。其他線程要等他創建完成後才能訪問SingletonFactory,且不會再new)。


       以上方法可以解決問題,同時也有人蔘考上面的思路來做了另一種,因爲上面主要就是讓訪問可以多線程同時,對象獲取只能單線程互斥,那把static class SingletonFactory換成一個synchronized的方法應該也可以吧,如下代碼:

public class SingletonTest {
	private static SingletonTest instance = null;
	private SingletonTest() {
	}
	private static synchronized void syncInit() {
		if (instance == null) {
			instance = new SingletonTest();
		}
	}
	public static SingletonTest getInstance() {
		if (instance == null) {
			syncInit();
		}
		return instance;
	}
}

     可能一眼看起來和synchronized塊的方法類似,這個是否也會發生類似的問題?(我的疑慮,不過貌似很難測者中情況,也許synchronized方法時會保證方法中的變量都創建對象並賦值)。


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