單例模式的五種實現方式及優缺點
公號:碼農充電站pro
主頁://codeshellme.github.io
當我們需要使得某個類只能有一個實例時,可以使用單例模式。
單例模式(Singleton Design Pattern)保證一個類只能有一個實例,並提供一個全局訪問點。
單例模式的實現需要三個必要的條件:
- 單例類的構造函數必須是私有的,這樣才能將類的創建權控制在類的內部,從而使得類的外部不能創建類的實例。
- 單例類通過一個私有的靜態變量來存儲其唯一實例。
- 單例類通過提供一個公開的靜態方法,使得外部使用者可以訪問類的唯一實例。
注意:
因為單例類的構造函數是私有的,所以單例類不能被繼承。
另外,實現單例類時,還需要考慮三個問題:
- 創建單例對象時,是否線程安全。
- 單例對象的創建,是否延時加載。
- 獲取單例對象時,是否需要加鎖(鎖會導致低性能)。
下面介紹五種實現單例模式的方式。
1,餓漢式
餓漢式的單例實現比較簡單,其在類加載的時候,靜態實例instance
就已創建並初始化好了。
代碼如下:
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton () {}
public static Singleton getInstance() {
return instance;
}
}
餓漢式單例優缺點:
- 優點:
- 單例對象的創建是線程安全的;
- 獲取單例對象時不需要加鎖。
- 缺點:單例對象的創建,不是延時加載。
一般認為延時加載可以節省內存資源。但是延時加載是不是真正的好,要看實際的應用場景,而不一定所有的應用場景都需要延時加載。
2,懶漢式
與餓漢式對應的是懶漢式,懶漢式為了支持延時加載,將對象的創建延遲到了獲取對象的時候,但為了線程安全,不得不為獲取對象的操作加鎖,這就導致了低性能。
並且這把鎖只有在第一次創建對象時有用,而之後每次獲取對象,這把鎖都是一個累贅(雙重檢測對此進行了改進)。
代碼如下:
public class Singleton {
private static final Singleton instance;
private Singleton () {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懶漢式單例優缺點:
- 優點:
- 對象的創建是線程安全的。
- 支持延時加載。
- 缺點:獲取對象的操作被加上了鎖,影響了並發度。
- 如果單例對象需要頻繁使用,那這個缺點就是無法接受的。
- 如果單例對象不需要頻繁使用,那這個缺點也無傷大雅。
3,雙重檢測
餓漢式和懶漢式的單例都有缺點,雙重檢測的實現方式解決了這兩者的缺點。
雙重檢測將懶漢式中的 synchronized
方法改成了 synchronized
代碼塊。如下:
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) { // 注意這裡是類級別的鎖
if (instance == null) { // 這裡的檢測避免多線程並發時多次創建對象
instance = new Singleton();
}
}
}
return instance;
}
}
這種實現方式在 Java 1.4 及更早的版本中有些問題,就是指令重排序,可能會導致 Singleton
對象被 new
出來,並且賦值給 instance
之後,還沒來得及初始化,就被另一個線程使用了。
要解決這個問題,需要給 instance
成員變量加上 volatile
關鍵字,從而禁止指令重排序。
而高版本的 Java 已在 JDK 內部解決了這個問題,所以高版本的 Java 不需要關注這個問題。
雙重檢測單例優點:
- 對象的創建是線程安全的。
- 支持延時加載。
- 獲取對象時不需要加鎖。
4,靜態內部類
用靜態內部類的方式實現單例類,利用了Java 靜態內部類的特性:
- Java 加載外部類的時候,不會創建內部類的實例,只有在外部類使用到內部類的時候才會創建內部類實例。
代碼如下:
public class Singleton {
private Singleton () {}
private static class SingletonInner {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonInner.instance;
}
}
SingletonInner
是一個靜態內部類,當外部類 Singleton
被加載的時候,並不會創建 SingletonInner
實例對象。
只有當調用 getInstance()
方法時,SingletonInner
才會被加載,這個時候才會創建 instance
。instance
的唯一性、創建過程的線程安全性,都由 JVM 來保證。
靜態內部類單例優點:
- 對象的創建是線程安全的。
- 支持延時加載。
- 獲取對象時不需要加鎖。
5,枚舉
用枚舉來實現單例,是最簡單的方式。這種實現方式通過 Java 枚舉類型本身的特性,保證了實例創建的線程安全性和實例的唯一性。
public enum Singleton {
INSTANCE; // 該對象全局唯一
}
6,多例模式
上面介紹了5 種單例模式的實現方式,下面作為對單例模式的擴展,再來介紹一下多例模式以及線程間唯一的單例模式。先來看下多例模式。
單例模式是指,一個類只能創建一個對象。那麼多例模式就是,一個類可以創建多個對象,但是對象個數可以控制。
對於多例模式,我們可以將類的實例都編上號,然後將實例存放在一個 Map
中。
代碼如下:
public class MultiInstance {
// 實例編號
private long instanceNum;
// 用於存放實例
private static final Map<Long, MultiInstance> ins = new HashMap<>();
static {
// 存放 3 個實例
ins.put(1L, new MultiInstance(1));
ins.put(2L, new MultiInstance(2));
ins.put(3L, new MultiInstance(3));
}
private MultiInstance(long n) {
this.instanceNum = n;
}
public MultiInstance getInstance(long n) {
return ins.get(n);
}
}
實際上,Java 中的枚舉就是一個「天然」的多例模式,其中的每一項代表一個實例,如下:
public enum MultiInstance {
ONE,
TWO,
THREE;
}
7,線程唯一的單例
一般情況下,我們所說的單例的作用範圍是進程唯一的,就是在一個進程範圍內,一個類只允許創建一個對象,進程內的多個線程之間也是共享同一個實例。
實際上,在Java 中,每個類加載器都定義了一個命名空間。所以我們這裡實現的單例是依賴類加載器的,也就是在同一個類加載器中,我們實現的單例就是真正的單例模式。否則如果有多個類加載器,就會有多個單例出現了。一個解決辦法是:自行指定類加載器,並且指定同一個類加載器。
那麼線程唯一的單例就是,一個實例只能被一個線程擁有,一個進程內的多個線程擁有不同的類實例。
我們同樣可以用 Map
來實現,代碼如下:
public class ThreadSingleton {
private static final ConcurrentHashMap<Long, ThreadSingleton> instances
= new ConcurrentHashMap<>();
private ThreadSingleton() {}
public static ThreadSingleton getInstance() {
Long id = Thread.currentThread().getId();
instances.putIfAbsent(id, new ThreadSingleton());
return instances.get(id);
}
}
8,使用場景
單例模式可以用來管理一些共享資源,比如數據庫連接池,線程池;解決資源衝突問題,比如日誌打印。節省內存空間,比如配置信息類。
(本節完。)
推薦閱讀:
歡迎關注作者公眾號,獲取更多技術乾貨。