一天一個設計模式——(3)單例模式

(三)單例模式

單例模式(Singleton Pattern)指一個類確保在任何情況下只會有一個例項,並提供一個全域性訪問點 。

單例類擁有一個私有的建構函式,確保不能透過new關鍵字進行新建物件;同時單例類有一個全域性的靜態變數以及一個靜態工廠方法建立例項,並存儲在靜態變數中。

1,單例模式設計原則

單例類只能有一個例項;

單例類必須自己建立自己的例項;

單例類必須給所有物件提供這個例項。

一天一個設計模式——(3)單例模式

2,簡單案例

2。1,餓漢式單例

按照上述方式建立一個單例模式demo:

public class HungryStaticSingleton { //   1,提供唯一(static)例項,     private static final HungryStaticSingleton hungryStaticSingleton = new HungryStaticSingleton(); //   2,私有構造器,不能new的方式建立物件,只能自己提供     private HungryStaticSingleton(){    }; //   3,獲取例項,全域性(static)唯一入口     public static HungryStaticSingleton getInstance(){             return hungryStaticSingleton;    } }

上述方式其實建立的是一個餓漢式單例,可以看到他在類載入時就初始化建立了物件。這樣就絕對的執行緒安全。但是這樣很容易造成資源浪費,如果系統中有大量的單例物件,他們都在系統初始化時建立,將會導致系統記憶體不可控。因為不管物件使用與否,都預先進行了建立,將會佔用記憶體空間。

2。2,懶漢式單例

public class LazySimpleSingleton {     private LazySimpleSingleton(){}     private static  LazySimpleSingleton lazy = null;     public static LazySimpleSingleton getInstance(){         if (lazy==null){             lazy = new LazySimpleSingleton();        }         return lazy;    } }

可以看到,單例只是在呼叫時為空的情況下才被建立。但這樣會有執行緒安全問題。當兩個執行緒同時進入if判斷時仍會有兩個物件。解決機制可以在getInstance方法上新增同步鎖synchronized。但是我們知道當執行緒數量增加時,同步鎖會導致大量的執行緒處於阻塞狀態造成系統性能下降。

2。3,最佳化一之基於鎖的雙重檢查機制

上例相當於大量的執行緒擋在了方法之外,我們將執行緒放進來,放進if來,做兩次檢查:

public static LazySimpleSingleton getInstance() {         if (lazy == null) {             synchronized (LazySimpleSingleton。class){                 if(lazy == null){                     lazy = new LazySimpleSingleton();                }            }        }         return lazy;    }

這樣當第一個執行緒呼叫getInstance方法時,第二個執行緒也可以呼叫,只不過他們在if邏輯判斷這裡進行了二次分流,此時方法內部的阻塞對呼叫者來說並不會有明顯的感覺。但這樣同樣用到了鎖。

2。4,最佳化二之靜態內部類的方式

public class LazyStaticInnerClassSingleton {     private LazyStaticInnerClassSingleton(){             }     private static LazyStaticInnerClassSingleton getInstance(){         return LazyHolder。INSTANCE;    }     private static class LazyHolder{         private static final LazyStaticInnerClassSingleton INSTANCE =                  new LazyStaticInnerClassSingleton();    } }

這樣既解決了餓漢式的記憶體浪費問題,也解決了鎖機制的效能問題。因為靜態內部類在編譯以後產生一個新的位元組碼檔案,預設不載入,在使用的時候才會初始化;而靜態成員變數是在類載入的時候就會初始化。

但這樣做能不能透過反射拿到例項呢?

public class InnerTest {     public static void main(String[] args) {         try {             Class<?> clazz = LazyStaticInnerClassSingleton。class;             Constructor<?> c = clazz。getDeclaredConstructor(null); //           強制訪問             c。setAccessible(true); //           強制初始化             Object o1 = c。newInstance();             Object o2 = c。newInstance();             System。out。println(o1==o2);//false        } catch (Exception e) {             e。printStackTrace();        }    } }

可以看到,兩個物件記憶體地址不一樣,也就是反射機制破壞了單例模式。有種最佳化方法是在私有構造器中做一個判斷,只要第二次構造就丟擲執行時異常,阻止破壞單例模式。但這樣的方法並不優雅。

private LazyStaticInnerClassSingleton(){         if(LazyHolder。INSTANCE!=null){             throw new RuntimeException(“不允許構造兩個例項!”);        }    }

2。5,最佳化三之列舉類方式

public enum EnumSingleton {     INSTANCE;     private Object data;     public Object getData(){         return data;    }     public void setData(Object data){         this。data = data;    }     public static EnumSingleton getInstance(){         return INSTANCE;    } }

public class EnumSingletonTest {     public static void main(String[] args) {         EnumSingleton instance1 = null;         EnumSingleton instance2 = EnumSingleton。getInstance();         instance2。setData(new Object());          try {             FileOutputStream fos = new FileOutputStream(“EnumSingleton。obj”);             ObjectOutputStream oos = new ObjectOutputStream(fos);             oos。writeObject(instance2);             oos。flush();             oos。close();              FileInputStream fis = new FileInputStream(“EnumSingleton。obj”);             ObjectInputStream ois = new ObjectInputStream(fis);             instance1 = (EnumSingleton)ois。readObject();             ois。close();             System。out。println(instance1。getData());             System。out。println(instance2。getData());             System。out。println(instance1。getData()==instance2。getData());//true        } catch (Exception e) {             e。printStackTrace();        }    } }

可以看到列舉的方式能夠實現單例,而且能防止序列化和反序列化所產生的兩個物件問題。列舉的方式也是jdk推薦的解決單例問題的方案。透過檢視原始碼知道,列舉的方式底層也是類似餓漢式單例,也有記憶體佔用問題。

2。6,最佳化四之容器式單例

public class ContainerSingleton { private ContainerSingleton(){}; private static Map ioc = new ConcurrentHashMap<>(); public static Object getBean(String className){ synchronized (ioc){ if(!ioc。containsKey(className)){ Object obj = null; try { obj = Class。forName(className); ioc。put(className,obj); } catch (ClassNotFoundException e) { e。printStackTrace(); } return obj; }else { return ioc。get(className); } } }}

public class ContainerTest { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { System。out。println(Thread。currentThread()。getName()+“:” +ContainerSingleton。getBean(“com。lsk。creationalPatterns。singleton。ContainerSingleton”)); } }; new Thread(runnable)。start(); new Thread(runnable)。start(); }}

Thread-0:com。lsk。creationalPatterns。singleton。ContainerSingleton@3d6741a8Thread-1:com。lsk。creationalPatterns。singleton。ContainerSingleton@3d6741a8

可以看到這種建立方式由map型容器負責建立單例物件,建立一次之後每次從map裡獲取即可。當需要建立大量的單例物件時,容器式單例就能體現出它的優勢了。

3,單例模式的點評

單例模式確保了全域性只有一個例項物件,因此它適用於以下場景:

需要頻繁建立和銷燬的物件以及物件的建立非常消耗資源等,使用單例能夠很好地節約系統資源。比如資料庫連線池。

系統只需要一個例項,比如建立唯一的序列號場景;

系統只想提供一個公共訪問點,不能透過其他途徑訪問。

但是單例模式沒有介面,擴充套件困難;而且全域性唯一入口,單例類職責過重,違背單一原則。