單例模式寫法大全,也許有你不知道的寫法
導航
- 引言
- 什麼是單例?
- 單例模式作用
- 單例模式的實現方法
引言
單例模式想必是大家接觸的比較多的一種模式了,就算沒用過但是肯定聽過他的鼎鼎大名了。在我初入程式設計界時聽到最多的就是單例模式,工廠模式,觀察者模式了。特別是觀察者模式在Android開發中幾乎是隨處可見,不過今天我們先來學習一個看似簡單很多的單例模式。
什麼是單例模式?
單例模式確保某一個類只有一個例項。
單例模式有什麼用?
為什麼要確保一個類只有一個例項?有什麼時候才需要用到單例模式呢?聽起來一個類只有一個例項好像沒什麼用呢! 那我們來舉個例子。比如我們的APP中有一個類用來儲存執行時全域性的一些狀態資訊,如果這個類實現不是單例的,那麼App裡面的元件能夠隨意的生成多個類用來儲存自己的狀態,等於大家各玩各的,那這個全域性的狀態資訊就成了笑話了。而如果把這個類實現成單例的,那麼不管App的哪個元件獲取到的都是同一個物件(比如Application類,除了多程式的情況下)。
怎麼實現單例模式?
單例模式的定義和功能都是比較簡單清楚的東西,那麼到底怎麼實現這個模式呢?
1.餓漢式
可能有的小夥伴們會想到利用Java的靜態域初始化機制來實現
public class SimpleSingleton {
private static SimpleSingleton instance=new SimpleSingleton();
/**
* 構造方法私有化,幾乎所有的單例模式實現都會將構造方法私有化
*/
private SimpleSingleton() {
}
public static SimpleSingleton getInstance(){
return instance;
}
}
複製程式碼
這種寫法很簡單,先把建構函式設為private的(幾乎大部分的單例寫法都會這麼做)。然後在類中設定一個靜態的欄位,並呼叫建構函式。這樣jvm在載入這個類的時候就會自動初始化這個類,接著每次需要使用的時候都呼叫getInstance方法獲得類例項。而java的語法規則保證new SimpleSingleton()自會呼叫一次。
使用這種方式實現的單例是執行緒安全的(這一點是由jvm保證的),並且在類載入的時候就已經生成了一個例項,當呼叫的時候get獲取這個例項是就非常快。
凡事都有優缺點,餓漢式單例也不例外。他的一個很明顯的缺點就是在效能上。jvm會在載入類的時候直接初始化例項,而如果這個類的例項在應用中使用頻率並不高,有的時候整個App從被使用者開啟到結束都不會使用一次這個類例項,那麼這個初始化的操作就是完全浪費了。
為了解決這個問題,我們想了一種新的實現方式。
2.懶漢式
public class ServiceNotThreadSafe {
public static ServiceNotThreadSafe INSTANCE = null;
/**
* 必備操作
*/
private ServiceNotThreadSafe() {
}
/**
* @return instance
*/
public static ServiceNotThreadSafe getInstance() {
if (INSTANCE == null) {
INSTANCE = new ServiceNotThreadSafe();
}
return INSTANCE;
}
}
複製程式碼
懶漢式的寫法有一個懶載入的效果,只有當第一次呼叫getInstance方法時才會去例項化一個物件。可能細心的小夥伴已經注意到這個很直白的類名了NotThreadSafe。沒錯這種寫法在單執行緒中沒有任何問題,但是在併發程式中就無法保證 類例項只有一個的情況了。
為了解決上面的問題,我們相出了一個新的寫法
3.懶漢式 —— 單鎖定法
public class ServiceThreadSafe {
public static ServiceThreadSafe instance;
/**
* 還是常規操作的私有建構函式
*/
private ServiceThreadSafe() {
}
public static synchronized ServiceThreadSafe getInstance(){
if (instance==null){
instance=new ServiceThreadSafe();
}
return instance;
}
}
複製程式碼
這種寫法沒什麼好說的,只是在getInstance方法上加了一個內建同步鎖,從而保證了執行緒安全。但是也因此引入了一個新的問題 —— 同步鎖範圍太大,影響併發效能(在getInstance方法並不是頻繁呼叫下問題不大)。
為了解決這個問題,聰明的程式設計師們想到了另一種寫法。
4.雙重鎖定法
public class ServiceDoubleCheck {
/**
* 注意這裡加了 volatile 修飾符,用來保證記憶體可見性(限制指令重排序)。
* 具體各位小夥伴可以Google一下。
* 如果你沒加這個修飾符的話,那麼具體結果只能看編譯器,jvm和cpu的心情了O(∩_∩)O~~
*/
public static volatile ServiceDoubleCheck instance = null;
/**
* 大家都懂得操作
*/
private ServiceDoubleCheck() {
}
/**
* 理論上只要第一次的時候才會完全走完整個方法,之後進入這個方法時instance==null都不成立
* 而不用在進入內部的同步程式碼塊,帶來新能上的優勢
* @return
*/
public static ServiceDoubleCheck getInstance() {
if (instance == null) {
synchronized (ServiceDoubleCheck.class) {
if (instance == null) {
instance = new ServiceDoubleCheck();
}
}
}
return instance;
}
}
複製程式碼
優點:
- 資源利用率高,懶載入的形式,不使用就不會例項化
- 執行緒安全 缺點:
- 寫法略微繁瑣
- 第一次載入時速度不快
5.懶漢式靜態內部類寫法
public class ServiceInner {
/**
* 實現一個靜態內部類
*/
private static class Instance{
private static ServiceInner instance=new ServiceInner();
}
public static ServiceInner getInstance(){
return Instance.instance;
}
private ServiceInner() {
}
}
複製程式碼
優點:
- 執行緒安全
- 懶載入形式,資源利用率高 缺點:
- 第一次載入速度不快
6.列舉實現 —— 一個《 Effective Java 》 作者都推薦的方法
public class Resources {
public enum ResourcesInstance {
INSTANCE;
private Resources instance;
ResourcesInstance() {
this.instance = new Resources();
}
public Resources getInstance() {
return instance;
}
}
}
複製程式碼
之前介紹過的哪些單例實現都有一個問題,就是不能保證序列化生成另一個例項。比如先序列化寫入到檔案,然後再從檔案讀取反序列化回來,這樣子我們就會得到兩個例項,這就違背了單例的原則。而用列舉實現能解決這個問題。
總結
基本上單例的實現方法都介紹完了,一般實際應用中如果物件的例項化並不是很耗費資源的話使用最簡單的餓漢法就行了。如果需要物件懶載入則可以選用雙重鎖定法(如果不需要考慮執行緒安全的話可以使用簡單的懶漢式)。而要在序列化的過程中保證單例的話就要使用列舉的方法來實現了。