前言
作爲一個好學習的程序開發者,應該會去學習優秀的開源框架,當然學習的過程中不免會去閱讀源碼,這也是一個優秀程序員的必備素養,在學習的過程中很多人會遇到的障礙,那就是設計模式。很多優秀的框架會運用設計模式來達到事半功倍的效果。鑑於自己之前對設計模式的生疏,在閱讀源碼時遇到設計模式的巧妙運用理解比較吃力。最近搞了一本新書 《圖解設計模式》(目測講的很基礎)開始學習設計模式,對今後學習源碼打下堅實的基礎。後續我在閱讀本書的過程中,我將記錄下自己學習總結。今天就從最常使用的單例模式說起。
單例模式
在許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行爲。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。(維基百科)。
懶漢式
在單例模式中,有一種稱爲懶漢式的單例模式。顧名思義,懶漢式可以理解使用時才進行初始化,它包括私有的構造方法,私有的全局靜態變量,公有的靜態方法,是一種懶加載機制。
public class Singleton {
private static Singleton instance;
private Singleton() {
System.out.println("初始化");
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面是最常見的懶漢式單例模式的寫法,但是如果在多線程的情況下,上述方法就會出現問題,它達不到只有一個單例對象的效果,例如當某個線程1調用getInstance()方法並判斷instance == null
,此時(就在判斷爲空後new Singleton()之前)另一個線程2也調用getInstance()方法,由於此時線程1還沒有new出對象,則線程2執行getInstance()中instance 也爲空,那麼此時就會出現多個實例的情況,而達不到只有一個實例的目的。
懶漢式(線程安全)
在上述實現中我們提到的懶漢式單例模式是一種非線程安全的,非線程安全即多線程訪問時會生成多個實例。那麼怎麼樣實現線程安全呢,也許你應該已經想到使用同步關鍵字synchronized。
public class Singleton {
private static Singleton instance;
private Singleton() {
System.out.println("初始化");
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
使用同步關鍵字後,也就實現了線程安全訪問,因爲在任何時候它只能有一個線程調用 getInstance() 方法。那麼你可能會發出疑問,這樣加入同步,在高併發情況下,效率是很低的,因爲真正需要同步的是我們第一次初始化的時候,是的,所以我們要進行進一步的優化。
雙重檢測機制
雙重檢測顧名思義就是兩次檢測,一次是檢測instance 實例是否爲空,進行第一次過濾,在同步快中進行第二次檢測,因爲當多個線程執行第一次檢測通過後會同時進入同步快,那麼此時就有必要進行第二次檢測來避免生成多個實例。
public class Singleton {
private static Singleton instance;
private Singleton() {
System.out.println("初始化");
}
public static Singleton getInstance() {
if(instance==null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
對於上面的代碼時近乎完美的,既然說近乎完美,那肯定還是有瑕疵的,瑕疵出現的原因就是instance = new Singleton();這一句代碼,你可能會問,這會有什麼問題,其實我也不知道,哈哈。在計算機語言中,初始化包含了三個步驟
- 分配內存
- 執行構造方法初始化
- 將對象指向分配的內存空間
由於java編譯器爲了儘可能減少內存操作速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照自己的一些規則(這規則後面再敘述)將程序編寫順序打亂——即寫在後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行——以儘可能充分地利用CPU就會出現指令重排序(happen-before),從而導致上面的三個步驟執行順序發生改變。正常情況下是123,但是如果指令重排後執行爲1,3,2那麼久會導致instance 爲空,進而導致程序出現問題。
既然已經知道了上述雙重檢測機制會出現問題,那麼我們該怎麼避免出現呢,該如何解決呢,最好的辦法就是不要使用,開個玩笑啦。java中有一個關鍵字volatile,他有一個作用就是防止指令重排序,那麼我們把singleton用volatile修飾下就可以了,如下。
private volatile static Singleton singleton;
餓漢式
餓漢式與懶漢式區別是它在類加載的時候就進行初始化操作,而懶漢式是調用getInstance()方法時才進行初始化。
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton() {
System.out.println("初始化");
}
public static Singleton getInstance(){
return instance;
}
}
與懶漢式相比,它是線程安全的(無需用同步關鍵字修飾),由於沒有加鎖,執行效率也相對較高,但是也有一些缺點,在類加載時就初始化,會浪費內存。
靜態塊實現方式
public class HungrySingleton implements Serializable {
private static HungrySingleton instance = null;
static {
instance = new HungrySingleton();
}
private HungrySingleton() {
System.out.println("初始化");
}
public static HungrySingleton getInstance(){
return instance;
}
}
靜態內部類實現方式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
System.out.println("初始化");
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚舉方式
public enum EnumSinglton {
INSTANCE;
private EnumSinglton() {
System.out.println("構造方法");
}
public void doSomething() {
System.out.println("調用單例方法");
}
}
枚舉方式實現的單例模式是一種線程安全的單例模式。
序列化對單例模式的影響
通過上面我們對單例模式的學習,對單例模式有了進一步的學習,當我們有序列化的需求之後,那麼會產生怎樣的效果呢?先通過下面的代碼來看下結果
public class Main {
public static void main(String[] args) throws Exception {
//測試序列化對單例模式的影響
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(LazySingleton.getInstance());
ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
LazySingleton singleton = (LazySingleton) new ObjectInputStream(bin).readObject();
System.out.println(singleton == LazySingleton.getInstance());//false
System.out.println( EnumSinglton.INSTANCE == EnumSinglton.INSTANCE );//true
}
}
通過我們的測試瞬時就懵逼了,除了通過枚舉方式實現的單例模式,其它幾種實現都生成了多個實例。那麼該如何解決呢?難道反序列話只能生成多個實例。當然不是。我們可以看下面修改後的代碼
public class HungrySingleton implements Serializable {
private static HungrySingleton instance = new HungrySingleton();
//餓漢式變種5
static {
instance = new HungrySingleton();
}
private HungrySingleton() {
System.out.println("初始化");
}
public static HungrySingleton getInstance(){
return instance;
}
/**
* 如果序列化,需要加入此方法,否則單例模式無效
* @see java.io.ObjectStreamClass
* @return
*/
private Object readResolve() {
return instance;
}
}
靜態內部類相對實現較爲簡單,並且它是一種懶加載機制, 當Singleton 類被裝載了,instance 不一定被初始化。因爲 SingletonHolder 類沒有被主動使用,只有顯示通過調用 getInstance 方法時,纔會顯示裝載 SingletonHolder 類,從而實例化 instance。
好了,關於單例模式到這裏已經介紹完畢了。有問題歡迎留言指出,Have a wonderful day .