概述
單例模式算是我接觸設(shè)計(jì)模式這種思想所學(xué)習(xí)的第一個(gè)設(shè)計(jì)模式。記得剛?cè)胄袝r(shí)面試器腋,面試官總是會(huì)讓寫一種單例模式的實(shí)現(xiàn)溪猿。這篇文章主要是來總結(jié)一下單例模式的幾種實(shí)現(xiàn)以及每一種實(shí)現(xiàn)的優(yōu)缺點(diǎn),旨在領(lǐng)會(huì)每一種寫法纫塌,真正明白他們的區(qū)別诊县,免得以后尷尬。Mark措左。
定義
按照設(shè)計(jì)模式中的定義依痊,Singleton模式的用途是"ensure a class has only one instance, and provide a global
point of access to it"
(確保每個(gè)類只有一個(gè)實(shí)例,并提供它的全局訪問點(diǎn))
故名思義怎披,就是這個(gè)類在當(dāng)次整個(gè)系統(tǒng)中只存在一個(gè)實(shí)例胸嘁,所有的訪問必須通過這個(gè)唯一的實(shí)例來進(jìn)行調(diào)用
一、懶漢式
之所以稱為懶漢式凉逛,是基于這個(gè)類的實(shí)例是否能夠按需加載性宏,也就是懶加載。
代碼實(shí)現(xiàn)
public class LazySingleton {
private static LazySingleton lazySingleton;
privite LazySingleton(){}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
這種是最基本的實(shí)現(xiàn)鱼炒,但是這種方式在多線程并發(fā)中是有問題的衔沼,不是唯一的實(shí)例。
假如A/B兩個(gè)線程第一次同時(shí)訪問這個(gè)getInstance方法獲得實(shí)例昔瞧,然而這個(gè)條件(instance == null)有可能同時(shí)都成立(并發(fā)執(zhí)行)指蚁,那么線程A和線程B會(huì)分別獲得一個(gè)類的對(duì)象,這樣的話自晰,這個(gè)類的單例就失去意義了凝化。
策略評(píng)價(jià):
優(yōu)點(diǎn)是可以實(shí)現(xiàn)延遲加載。在類初次加載的時(shí)候酬荞,由于只是聲明了這個(gè)靜態(tài)的對(duì)象搓劫,但不會(huì)自動(dòng)初始化 instance對(duì)象,所以稱為懶漢式
缺點(diǎn)是線程不安全混巧。多線程并發(fā)無(wú)法保證唯一的實(shí)例
改進(jìn)策略:需要保證線程安全
二枪向、餓漢式
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton ();
privite HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton ;
}
}
策略評(píng)價(jià):
線程安全。利用了類的加載機(jī)制咧党,加載初始化靜態(tài)變量秘蛔,且被只會(huì)執(zhí)行一次,且JVM會(huì)利用鎖來同步多個(gè)線程對(duì)同一個(gè)類的初始化。這樣就保證了構(gòu)造方法只會(huì)調(diào)用一次深员。
不能懶加載负蠕。無(wú)論是否使用都會(huì)去初始化實(shí)例。
改進(jìn)策略:需要懶加載倦畅,請(qǐng)看懶漢線程安全式
注意到一點(diǎn)遮糖,懶漢模式在申明lazySingleton 的時(shí)候沒有加final關(guān)鍵字,但是餓漢模式加了叠赐。那餓漢模式為什么要加final欲账,什么時(shí)候加?
final關(guān)鍵詞是代表此變量一經(jīng)賦值芭概,其指向的內(nèi)存引用地址將不會(huì)再改變敬惦。加final也僅僅是表示類加載的時(shí)候就初始化對(duì)象了。比不加載final要早一點(diǎn)谈山。
如果存在釋放資源的情況下俄删,就不能加final修飾了,釋放資源之后奏路,如果需要重新使用這個(gè)單例畴椰,就必須存在重新初始化的過程,而final定義的常量是不能重新賦值的鸽粉,所以不能加final斜脂,對(duì)于不需要釋放資源的情況,可以加final
總而言之触机,要不要加final修飾帚戳,可以根據(jù)情況而定。
懶漢模式為什么不加final儡首,是因?yàn)楸籪inal修飾的變量需要直接賦值片任,或者在靜態(tài)代碼塊中賦值,這樣就不是懶加載模式了
三蔬胯、懶漢線程安全式
public class LazySafetySingleton {
private static LazySafetySingleton lazySafeSingleton;
privite LazySafetySingleton (){}
public static synchronized LazySafetySingleton getInstance(){
if(lazySafeSingleton== null){
lazySafeSingleton= new LazySafetySingleton ();
}
return lazySafeSingleton;
}
}
public class LazySafetySingleton{
private static LazySafetySingleton lazySafeSingleton;
privite LazySafetySingleton (){}
public static LazySafetySingleton getInstance() {
synchronized (LazySafetySingleton.class) {
if (lazySafeSingleton == null) {
lazySafeSingleton = new LazySafetySingleton();
}
}
return lazySafeSingleton ;
}
}
策略評(píng)價(jià):
線程安全对供。使用了synchronized同步鎖。
懶加載氛濒。不會(huì)在類加載就初始化产场。
顯然這種同步性能很低。由于使用了同步鎖舞竿,所以每次調(diào)用getInstance都會(huì)進(jìn)行同步京景。其實(shí)我們只想第一次(instance == null)進(jìn)行同步,初始化成功后骗奖,以后每次都直接返回就行了确徙。
改進(jìn)策略:DCL
四靡菇、double-check-locking
public class DclSingleton {
private volatile static DclSingleton dlcSingleton;
privite DclSingleton (){}
public static DclSingleton getInstance() {
if(dlcSingleton== null){
synchronized (DclSingleton .class){
if(dlcSingleton == null){
dlcSingleton = new DclSingleton();
}
}
}
return dlcSingleton ;
}
}
這種策略看似解決了每次都需要同步的問題,但是由于 標(biāo)記4處 instance = new DclSingleton(); 這個(gè)初始化是非原子性的操作米愿。就是說這個(gè)在JVM中可能會(huì)分成幾步執(zhí)行,那么就會(huì)存在指令重排序的問題,所以需要繼續(xù)改進(jìn) 聲明處添加 volatile 關(guān)鍵字鼻吮。
改進(jìn)后育苟,懶加載
線程安全
五、靜態(tài)內(nèi)部類
public class StaticInnerSingleton {
privite StaticInnerSingleton(){}
public static StaticInnerSingleton getinstance(){
return SingletonHolder.staticInnerSingleton;
}
public static class SingletonHolder{
private static final StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
}
}
策略評(píng)價(jià):
這種靜態(tài)內(nèi)部類的實(shí)現(xiàn)椎木,主要是運(yùn)用類的加載機(jī)制來保證線程的安全(原因見懶漢式的線程安全優(yōu)點(diǎn))违柏。同時(shí)這個(gè)內(nèi)部類與外部類沒有綁定關(guān)系,而且只有外部類調(diào)用的時(shí)候才會(huì)加載香椎,所以能做到懶加載漱竖。
但是如果實(shí)例化的時(shí)候需要傳參就很不方便了,比如需要傳context畜伐。所以不需要傳參的時(shí)候建議使用該方法
六馍惹、枚舉
public enum EnumSingleton {
INSTANCE;
public void doSomeThing() {
//在此處進(jìn)行實(shí)例化對(duì)象
}
}
策略評(píng)價(jià):枚舉實(shí)現(xiàn)是目前比較建議的方法,和他獲取單例的方法比起來玛界,有兩個(gè)明顯的優(yōu)勢(shì):
- 避免反射攻擊万矾,其他方式實(shí)現(xiàn)的單例都可以通過反射拿到構(gòu)造器,通過構(gòu)造器獲取實(shí)例慎框。但獲取的實(shí)例和通過正常方式獲取的實(shí)例不是同一個(gè)對(duì)象良狈。枚舉單例模式無(wú)法通過反射獲取實(shí)例,因?yàn)镃onstructor類的newInstance方法源碼中有判斷笨枯,要實(shí)例化的類是否是枚舉薪丁,如果不是,才能實(shí)例化(14行)
1 public T newInstance(Object ... initargs)
2 throws InstantiationException, IllegalAccessException,
3 IllegalArgumentException, InvocationTargetException
4 {
5 if (!override) {
6 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
7 Class<?> caller = Reflection.getCallerClass();
8 checkAccess(caller, clazz, null, modifiers);
9 }
10 }
11 if ((clazz.getModifiers() & Modifier.ENUM) != 0)
12 throw new IllegalArgumentException("Cannot reflectively create enum objects");
13 ConstructorAccessor ca = constructorAccessor; // read volatile
14 if (ca == null) {
15 ca = acquireConstructorAccessor();
16 }
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
- 避免序列化
傳統(tǒng)單例存在的另外一個(gè)問題是一旦你實(shí)現(xiàn)了序列化接口馅精,那么它們不再保持單例了严嗜,因?yàn)閞eadObject()方法一直返回一個(gè)新的對(duì)象就像java的構(gòu)造方法一樣。但枚舉實(shí)現(xiàn)的單例洲敢,即使序列化阻问,獲取的對(duì)象依然保持單例。