相信在設計模式中有一個經常提到的概念:單例模式,爲什麼它經常出現在面試話題中,因爲它的應用場景十分廣泛。
使用場景:
比如
- 數據庫連接池,作爲數據庫的緩存,避免頻繁連接關閉數據庫,
- Java線程池,控制管理線程。
- log4j日誌記錄,由始至終記錄着運行日誌。
定義:
保證系統中一個類只有一個實例,而且必須自己創建自己的唯一實例,該實例易於外界訪問,從而方便對實例個數的控制並節約系統資源。
創建單例模式的幾種方式以及比較
1. 餓漢模式
/*
1. 餓漢模式:
2. 優點:線程安全
3. 缺陷:性能低/加載類就初始化單例/不適合需要外部傳入參數配置的單例模式
*/
public class SingletonHungry {
private static final SingletonHungry instance = new SingletonHungry();
public static SingletonHungry getInstance() {
return instance;
}
public static void main(String[] args) {
SingletonHungry s1 = SingletonHungry.getInstance();
SingletonHungry s2 = SingletonHungry.getInstance();
System.out.print("餓漢模式實例對比:");
//true
System.out.println(s1.getInstance()==s2.getInstance());
}
}
由於餓漢模式在類內部創建實例,所以它是線程安全,正式它在類內部就靜態加載,所以它不能從外部傳入參數配置。
具體來看看懶漢模式.
2. 懶漢模式
package com.dd.code.singleton;
/*
* 懶漢模式
* 優點:簡單/對比餓漢,加載此單例可以外部傳入配置
* 缺陷:線程不安全
*/
public class SingletonLazy {
private static SingletonLazy instance;
/*
* 配置成員conf(假設必須傳入conf該單例纔可以加載)
* 這不能在類中優先初始化 private static final SingletonHungry instance = new SingletonHungry();
*/
private static String conf;
//外部傳入屬性配置
public static void setConf(String conf) {
SingletonLazy.conf = conf;
}
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
public static void main(String[] args) {
SingletonLazy.setConf("配置文件優先");
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.print("懶漢模式實例對比:");
//true
System.out.println(s1.getInstance()==s2.getInstance());
}
}
對比餓漢模式,懶漢模式可以傳入必要配置再手動實例化,但是由於手動實例化,則需要考慮線程安全問題。
3. 線程安全懶漢模式
/*
* 線程安全懶漢加載
* 優點:線程安全/可以外部傳配置
* 缺陷:代價較高,創建單例只需要第一次保證線程安全就好,不需要每次都同步
* 優化解決:SingletonDoubleCheck
*/
public class SingletonThreadSafe {
private static SingletonThreadSafe instance;
//加了同步關鍵字synchronized
private synchronized SingletonThreadSafe getInstance() {
if (instance == null) {
instance = new SingletonThreadSafe();
}
return instance;
}
public static void main(String[] args) {
SingletonThreadSafe s1 = new SingletonThreadSafe();
SingletonThreadSafe s2 = new SingletonThreadSafe();
System.out.print("線程安全懶漢加載實例對比:");
//true
System.out.println(s1.getInstance()==s2.getInstance());
}
}
加了同步關鍵字synchronized保證了線程安全,但是它的性能就降低了,而且其實創建單例只需要第一次保證線程安全就好,不需要每次都同步。
所以引入了新的優化,好像很厲害的雙重鎖檢測模式
4. 雙重鎖檢測單例
/*
* 雙重鎖單例模式(較複雜)
* 優點:可傳入配置/對比SingletonThreadSafe性能優化,只有在實例爲空(第一次實現同步)
* 爲什麼要第二次判斷instance是否爲空,因爲把synchronized放裏層的話,
* 有可能有多個線程進入了*臨界區*,synchronized只能保證臨界區每次由一個線程執行而已,
* 二次檢測可以讓其他線程下次不初始化,防止冗餘情況
*/
public class SingletonDoubleCheck {
/*
* volatile關鍵字:
* 要知道,instance = new SingletonDoubleCheck();不是原子性操作,
* 雖然volatile關鍵字不能保證原子性,但是可以禁止指令重排
* 保證了instance = new SingletonDoubleCheck() 這一行的有效執行順序
*/
private volatile static SingletonDoubleCheck instance;
private static SingletonDoubleCheck getInstance() {
if (instance == null) {
//非臨界區
synchronized (SingletonDoubleCheck.class) {
//*臨界區*
if (instance == null) {
instance = new SingletonDoubleCheck();
}
}
}
return instance;
}
public static void main(String[] args) {
SingletonDoubleCheck s1,s2;
s1 = SingletonDoubleCheck.getInstance();
s2 = SingletonDoubleCheck.getInstance();
System.out.print("雙重鎖單例加載實例對比:");
//true
System.out.println(s1==s2);
}
}
雙重鎖模式可以說是性能和線程安全的折中,它保證線程安全又保證不需要每次都控制同步,第一個判斷if (instance == null)用來攔截已經創建的線程。
主要複雜的是從非臨界區到臨界區的情況,即當未創建實例的時候:要知道synchronized關鍵字其實只能保證每次一個線程執行修飾代碼塊,並不能保證只有一個線程,假設有超過一個線程進入臨界區,此時如果線程一執行instance = new SingletonDoubleCheck(),則線程2下一次會根據第二個if (instance == null)判斷是否再次創建實例,所以第二個if其實相當於一個flag標記,它巧妙的避免了多個線程創建多個實例。
這種方式有點燒腦,官方推薦還有另一種創建方式,靜態內部類模式,比較推薦使用的一種。
5. 靜態內部類
/*
* 靜態內部類
*/
public class SingletonNested {
private static class SingletonHolder{
public static final SingletonNested HOLDER_INSTANCE = new SingletonNested();
}
public static SingletonNested getInstance() {
return SingletonHolder.HOLDER_INSTANCE;
}
public static void main(String[] args) {
SingletonNested s1, s2;
s1 = SingletonNested.getInstance();
s2 = SingletonNested.getInstance();
System.out.print("靜態內部類實例對比:");
//true
System.out.println(s1==s2);
}
}
靜態內部類創建單例的優點:
- 由jvm保證線程安全,不需要用到同步控制,性能較高;
- 因爲區別於懶加載把instance作爲靜態內部成員,所以類加載時不會實例化instance 只有getInstance調用纔會初始化。
參考鏈接: