設計模式中常用的單例模式,在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方法時會保證方法中的變量都創建對象並賦值)。