單例模式的五種實現方式及優缺點
阿新 • • 發佈:2020-12-23
> **公號:碼農充電站pro**
> **主頁:**
當我們需要使得某個類只能有一個例項時,可以使用**單例模式**。
**單例模式**(*Singleton Design Pattern*)保證一個類只能有一個例項,並提供一個全域性訪問點。
單例模式的實現需要**三個必要的條件**:
1. 單例類的**建構函式**必須是**私有的**,這樣才能將類的建立權控制在類的內部,從而使得類的外部不能建立類的例項。
2. 單例類通過一個**私有的靜態變數**來儲存其唯一例項。
3. 單例類通過提供一個**公開的靜態方法**,使得外部使用者可以訪問類的唯一例項。
> **注意:**
> 因為單例類的建構函式是私有的,所以單例類不能被繼承。
另外,實現單例類時,還需要考慮三個問題:
- 建立單例物件時,是否執行緒安全。
- 單例物件的建立,是否延時載入。
- 獲取單例物件時,是否需要加鎖(鎖會導致低效能)。
下面介紹五種實現單例模式的方式。
### 1,餓漢式
**餓漢式**的單例實現比較簡單,其在類載入的時候,靜態例項`instance` 就已建立並初始化好了。
程式碼如下:
```java
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton () {}
public static Singleton getInstance() {
return instance;
}
}
```
餓漢式單例優缺點:
- 優點:
- 單例物件的建立是執行緒安全的;
- 獲取單例物件時不需要加鎖。
- 缺點:單例物件的建立,不是延時載入。
一般認為延時載入可以節省記憶體資源。但是延時載入是不是真正的好,要看實際的應用場景,而不一定所有的應用場景都需要延時載入。
### 2,懶漢式
與餓漢式對應的是**懶漢式**,懶漢式為了支援延時載入,將物件的建立延遲到了獲取物件的時候,但為了執行緒安全,不得不為獲取物件的操作加鎖,這就導致了低效能。
並且這把鎖只有在第一次建立物件時有用,而之後每次獲取物件,這把鎖都是一個累贅(**雙重檢測**對此進行了改進)。
程式碼如下:
```java
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` 程式碼塊。如下:
```java
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 載入外部類的時候,不會建立內部類的例項,只有在外部類使用到內部類的時候才會建立內部類例項*。
程式碼如下:
```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** 列舉型別本身的特性,保證了例項建立的執行緒安全性和例項的唯一性。
```java
public enum Singleton {
INSTANCE; // 該物件全域性唯一
}
```
### 6,多例模式
上面介紹了5 種單例模式的實現方式,下面作為對單例模式的擴充套件,再來介紹一下**多例模式**以及**執行緒間唯一的單例模式**。先來看下多例模式。
單例模式是指,一個類只能建立一個物件。那麼**多例模式**就是,一個類可以建立多個物件,但是物件個數可以控制。
對於多例模式,我們可以將類的例項都編上號,然後將例項存放在一個 `Map` 中。
程式碼如下:
```java
public class MultiInstance {
// 例項編號
private long instanceNum;
// 用於存放例項
private static final Map 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** 中的列舉就是一個“天然”的多例模式,其中的每一項代表一個例項,如下:
```java
public enum MultiInstance {
ONE,
TWO,
THREE;
}
```
### 7,執行緒唯一的單例
一般情況下,我們所說的單例的作用範圍是程序唯一的,就是在一個程序範圍內,一個類只允許建立一個物件,程序內的多個執行緒之間也是共享同一個例項。
> 實際上,在Java 中,每個**類載入器**都定義了一個**名稱空間**。所以我們這裡實現的單例是依賴**類載入器**的,也就是在同一個類載入器中,我們實現的單例就是真正的單例模式。否則如果有多個類載入器,就會有多個單例出現了。一個解決辦法是:自行指定類載入器,並且指定同一個類載入器。
那麼**執行緒唯一的單例**就是,一個例項只能被一個執行緒擁有,一個程序內的多個執行緒擁有不同的類例項。
我們同樣可以用 `Map` 來實現,程式碼如下:
```java
public class ThreadSingleton {
private static final ConcurrentHashMap instances
= new ConcurrentHashMap<>();
private ThreadSingleton() {}
public static ThreadSingleton getInstance() {
Long id = Thread.currentThread().getId();
instances.putIfAbsent(id, new ThreadSingleton());
return instances.get(id);
}
}
```
### 8,使用場景
單例模式可以用來管理一些共享資源,比如資料庫連線池,執行緒池;解決資源衝突問題,比如日誌列印。節省記憶體空間,比如配置資訊類。
(本節完。)
---
**推薦閱讀:**
[設計模式之高質量程式碼](https://www.cnblogs.com/codeshell/p/13968620.html)
---
*歡迎關注作者公眾號,獲取更多技術乾貨。*
![碼農充電站pro](https://img-blog.csdnimg.cn/20200505082843773.png?#pic