單例模式你真的會了嗎(上篇)?
阿新 • • 發佈:2020-03-08
>單例模式相信是很多程式設計師接觸最多的了,也是面試過程中考察最頻繁的一個了,不知道你有沒有被問過這道面試題?歡迎留言討論。
今天我們來重點討論一下單例的幾個問題,及如何正確的實現一個單例,然後你再來回顧一下,你之前的回答或者使用方式是否正確。
## 為何要使用單例
單例非常簡單,一個類只允許建立一個物件或者例項,這個類就是一個單例類。這種設計模式就叫做單例設計模式,是建立型的第一種設計模式,簡稱單例模式。
單例模式什麼時候使用呢?又或者說這種情況下為什麼要使用單例?
1. 解決資源衝突問題
比如說,我們現在的java處理程式中是使用印表機,而我們的服務端程式是多執行緒的,但是印表機只有一個,不能重複建立印表機資源啊。當然我們也可以定義普通類,在呼叫列印新增synchronized關鍵字。
2. 全域性唯一類
有時候,我們做業務設計時,有些資料在系統中只應該保留一份,這時候就應該設計為單例。
比如配置資訊類,系統的配置檔案應該只有一份,載入到記憶體之後以物件的形式存在,理所應當只有一份。
再比如說,我們設計一個抽獎系統,每點選一次生成一個抽獎序號,可以設計一個單例,內部儲存好所有的序號,每次隨機取出一個序號。如果使用普通類物件的話,那就需要通過共享記憶體共享所有抽獎序號。
## 單例應該怎麼寫?
學習任何東西,因為大腦的容量是有限的,首先我們要理解概念,知道為什麼,來後追求怎麼做,怎麼實現,做的過程可能很複雜,比如有一二三四五步驟,但我們要化繁為簡,概括精簡。
單例需要考慮以下幾個問題:
- 建構函式要是private的,這樣才能避免外部通過new建立例項嘛,不然怎麼叫單例,別人可以隨便通過new來建立啊。
- 多執行緒建立時是否有執行緒安全問題。
- 支援延遲載入嗎?
- getInstance()效能高嗎?
## 單例典型實現方式
### 餓漢式
通過這種形容方式,可以直觀的理解一下,餓漢一直擔心自己吃不飽,所以先吃了再說,也就是說例項是事先初始化好的,也就沒有辦法延遲載入了。
不支援懶載入,有人就說這種方式不好,說我都沒有使用單例,你都給我載入了,浪費啊。但是有壞處也有好處,提前把類載入進來,提前暴露問題,這樣如果類的設計有問題,在程式啟動時就會報錯,而不是等到程式執行中才暴露出來。
```java
public class SingleTon {
private static final SingleTon instance = new SingleTon();
private SingleTon() {}
public static SingleTon getInstance() {
return instance;
}
public void method() {}
}
```
### 懶漢式
所謂懶漢式,那就是支援延遲載入嘍。總體思路類似,但在類內部並不是預設就把instance例項化好。
```java
public class SingleTon {
private static SingleTon instance;
private SingleTon() {}
public static synchronized SingleTon getInstance() {
if (instance == null) {
instance = new SingleTon();
}
return instance;
}
public void method() {}
}
```
為什麼要加synchronized呢?如果是多執行緒同時呼叫getInstance(),會有併發問題啊,多個執行緒可能同時拿到instance == null的判斷,這樣就會重複例項化,單例就不是單例。所以為了解決多執行緒併發的問題,這裡犧牲了效能,變成了嚴格的序列制。多執行緒下效能很低。
### 雙重檢測懶漢式
餓漢方式不支援延遲載入。
懶漢方式,多執行緒下效能低下,那怎麼修改呢,就是改進的懶漢方式,又叫雙重檢測。
具體怎麼做呢?
```java
public class SingleTon {
private static volatile SingleTon instance;
private SingleTon() {}
public static SingleTon getInstance() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
public void method() {}
}
```
這個類裡的volatile十分關鍵,如果沒有volatile關鍵字修飾instance變數,如果執行緒1執行到instance = new SingleTon();的時候,執行緒2此時判斷instance已經不等於null了,會直接返回instance,但此時instance並未初始化完畢,為什麼這麼說呢?因為物件的初始化分為三步:
- 分配記憶體
- 記憶體初始化
- 物件指向新分配的記憶體地址
既然是分為三步,那就不是原子操作,而且可能會發生指令重排,也就是說可能先執行第三步,這時候其他執行緒判斷instance也就不是null了。加上volatile關鍵字,可以禁止機器指令重排,就不會有這個問題了。
### 靜態內部類
這種方式,避免雙重檢測,利用java靜態內部類,類似餓漢方式,又做到延遲載入。
```java
public class SingleTon {
private SingleTon() {}
private static class SingleTonHolder {
private static final SingleTon instance = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHolder.instance;
}
public void method() {}
}
```
是不是覺得很簡潔?推薦大家使用這種方式,類SingleTon載入時,並不會載入SingleTonHolder類,只要呼叫getInstance方法時,SingleTonHolder才會被載入,並建立instance,這些都是由JVM來保證的。
### 列舉方式
還有一種更簡單的,但是理解起來可能有點費解,列舉的建構函式預設就是私有的。java的列舉型別本身就保證了執行緒安全性和例項唯一性。
只需要簡單幾行,就可以使用列舉單例INSTANCE的方法了。
```java
public enum SingleTon {
INSTANCE;
public void method() {}
}
```
但是單例模式真的就好嗎?下面我們會討論一下為什麼不推薦單例模式?如何替代,以及如何做到叢集下的分散式單例模式?
程式設計師的小夥伴們,學習之路,同行的人越多才可以走的更遠,加入公眾號[程式設計師之道],一起交流溝通,走出我們的程式設計師之道!
![掃碼加入吧!](https://img-blog.csdnimg.cn/201911011521127