設(shè)計(jì)模式之單例模式詳解
單例模式寫法大全,也許有你不知道的寫法
導(dǎo)航
- 引言
- 什么是單例?
- 單例模式作用
- 單例模式的實(shí)現(xiàn)方法
引言
單例模式想必是大家接觸的比較多的一種模式了,就算沒用過但是肯定聽過他的鼎鼎大名了。在我初入編程界時(shí)聽到最多的就是單例模式,工廠模式,觀察者模式了。特別是觀察者模式在Android開發(fā)中幾乎是隨處可見,不過今天我們先來學(xué)習(xí)一個(gè)看似簡(jiǎn)單很多的單例模式。
什么是單例模式?
單例模式確保某一個(gè)類只有一個(gè)實(shí)例。
單例模式有什么用?
為什么要確保一個(gè)類只有一個(gè)實(shí)例?有什么時(shí)候才需要用到單例模式呢?聽起來一個(gè)類只有一個(gè)實(shí)例好像沒什么用呢!
那我們來舉個(gè)例子。比如我們的APP中有一個(gè)類用來保存運(yùn)行時(shí)全局的一些狀態(tài)信息,如果這個(gè)類實(shí)現(xiàn)不是單例的,那么App里面的組件能夠隨意的生成多個(gè)類用來保存自己的狀態(tài),等于大家各玩各的,那這個(gè)全局的狀態(tài)信息就成了笑話了。而如果把這個(gè)類實(shí)現(xiàn)成單例的,那么不管App的哪個(gè)組件獲取到的都是同一個(gè)對(duì)象(比如Application類,除了多進(jìn)程的情況下)。
怎么實(shí)現(xiàn)單例模式?
單例模式的定義和功能都是比較簡(jiǎn)單清楚的東西,那么到底怎么實(shí)現(xiàn)這個(gè)模式呢?
1.餓漢式
可能有的小伙伴們會(huì)想到利用Java的靜態(tài)域初始化機(jī)制來實(shí)現(xiàn)
public class SimpleSingleton {
private static SimpleSingleton instance=new SimpleSingleton();
/**
* 構(gòu)造方法私有化,幾乎所有的單例模式實(shí)現(xiàn)都會(huì)將構(gòu)造方法私有化
*/
private SimpleSingleton() {
}
public static SimpleSingleton getInstance(){
return instance;
}
}
??這種寫法很簡(jiǎn)單,先把構(gòu)造函數(shù)設(shè)為private的(幾乎大部分的單例寫法都會(huì)這么做)。然后在類中設(shè)置一個(gè)靜態(tài)的字段,并調(diào)用構(gòu)造函數(shù)。這樣jvm在加載這個(gè)類的時(shí)候就會(huì)自動(dòng)初始化這個(gè)類,接著每次需要使用的時(shí)候都調(diào)用getInstance方法獲得類實(shí)例。而java的語法規(guī)則保證new SimpleSingleton()自會(huì)調(diào)用一次。
??使用這種方式實(shí)現(xiàn)的單例是線程安全的(這一點(diǎn)是由jvm保證的),并且在類加載的時(shí)候就已經(jīng)生成了一個(gè)實(shí)例,當(dāng)調(diào)用的時(shí)候get獲取這個(gè)實(shí)例是就非??臁?/p>
凡事都有優(yōu)缺點(diǎn),餓漢式單例也不例外。他的一個(gè)很明顯的缺點(diǎn)就是在性能上。jvm會(huì)在加載類的時(shí)候直接初始化實(shí)例,而如果這個(gè)類的實(shí)例在應(yīng)用中使用頻率并不高,有的時(shí)候整個(gè)App從被用戶打開到結(jié)束都不會(huì)使用一次這個(gè)類實(shí)例,那么這個(gè)初始化的操作就是完全浪費(fèi)了。
為了解決這個(gè)問題,我們想了一種新的實(shí)現(xiàn)方式。
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;
}
}
懶漢式的寫法有一個(gè)懶加載的效果,只有當(dāng)?shù)谝淮握{(diào)用getInstance方法時(shí)才會(huì)去實(shí)例化一個(gè)對(duì)象??赡芗?xì)心的小伙伴已經(jīng)注意到這個(gè)很直白的類名了NotThreadSafe。沒錯(cuò)這種寫法在單線程中沒有任何問題,但是在并發(fā)程序中就無法保證 類實(shí)例只有一個(gè)的情況了。
為了解決上面的問題,我們相出了一個(gè)新的寫法
3.懶漢式 —— 單鎖定法
public class ServiceThreadSafe {
public static ServiceThreadSafe instance;
/**
* 還是常規(guī)操作的私有構(gòu)造函數(shù)
*/
private ServiceThreadSafe() {
}
public static synchronized ServiceThreadSafe getInstance(){
if (instance==null){
instance=new ServiceThreadSafe();
}
return instance;
}
}
這種寫法沒什么好說的,只是在getInstance方法上加了一個(gè)內(nèi)置同步鎖,從而保證了線程安全。但是也因此引入了一個(gè)新的問題 —— 同步鎖范圍太大,影響并發(fā)性能(在getInstance方法并不是頻繁調(diào)用下問題不大)。
為了解決這個(gè)問題,聰明的程序員們想到了另一種寫法。
4.雙重鎖定法
public class ServiceDoubleCheck {
/**
* 注意這里加了 volatile 修飾符,用來保證內(nèi)存可見性(限制指令重排序)。
* 具體各位小伙伴可以Google一下。
* 如果你沒加這個(gè)修飾符的話,那么具體結(jié)果只能看編譯器,jvm和cpu的心情了O(∩_∩)O~~
*/
public static volatile ServiceDoubleCheck instance = null;
/**
* 大家都懂得操作
*/
private ServiceDoubleCheck() {
}
/**
* 理論上只要第一次的時(shí)候才會(huì)完全走完整個(gè)方法,之后進(jìn)入這個(gè)方法時(shí)instance==null都不成立
* 而不用在進(jìn)入內(nèi)部的同步代碼塊,帶來新能上的優(yōu)勢(shì)
* @return
*/
public static ServiceDoubleCheck getInstance() {
if (instance == null) {
synchronized (ServiceDoubleCheck.class) {
if (instance == null) {
instance = new ServiceDoubleCheck();
}
}
}
return instance;
}
}
優(yōu)點(diǎn):
- 資源利用率高,懶加載的形式,不使用就不會(huì)實(shí)例化
- 線程安全
缺點(diǎn): - 寫法略微繁瑣
- 第一次加載時(shí)速度不快
5.懶漢式靜態(tài)內(nèi)部類寫法
public class ServiceInner {
/**
* 實(shí)現(xiàn)一個(gè)靜態(tài)內(nèi)部類
*/
private static class Instance{
private static ServiceInner instance=new ServiceInner();
}
public static ServiceInner getInstance(){
return Instance.instance;
}
private ServiceInner() {
}
}
優(yōu)點(diǎn):
- 線程安全
- 懶加載形式,資源利用率高
缺點(diǎn): - 第一次加載速度不快
6.枚舉實(shí)現(xiàn) —— 一個(gè)《effective java》 作者都推薦的方法
public class Resources {
public enum ResourcesInstance {
INSTANCE;
private Resources instance;
ResourcesInstance() {
this.instance = new Resources();
}
public Resources getInstance() {
return instance;
}
}
}
之前介紹過的哪些單例實(shí)現(xiàn)都有一個(gè)問題,就是不能保證序列化生成另一個(gè)實(shí)例。比如先序列化寫入到文件,然后再從文件讀取反序列化回來,這樣子我們就會(huì)得到兩個(gè)實(shí)例,這就違背了單例的原則。而用枚舉實(shí)現(xiàn)能解決這個(gè)問題。
總結(jié)
基本上單例的實(shí)現(xiàn)方法都介紹完了,一般實(shí)際應(yīng)用中如果對(duì)象的實(shí)例化并不是很耗費(fèi)資源的話使用最簡(jiǎn)單的餓漢法就行了。如果需要對(duì)象懶加載則可以選用雙重鎖定法(如果不需要考慮線程安全的話可以使用簡(jiǎn)單的懶漢式)。而要在序列化的過程中保證單例的話就要使用枚舉的方法來實(shí)現(xiàn)了。