首先什么是單例在跳?就一條基本原則拧篮,單例對(duì)象的類只會(huì)被初始化一次。在 Java 中螺垢,我們可以說(shuō)在 JVM 中只存在該類的唯一一個(gè)對(duì)象實(shí)例喧务。而要實(shí)現(xiàn)一個(gè)安全的單例對(duì)象,需要考慮一下幾個(gè)問(wèn)題:
你的單例線程安全嗎?
你的單例反射安全嗎枉圃?
你的單例序列化安全嗎功茴?
一、單例的一般實(shí)現(xiàn)
1孽亲、餓漢式
public class HungrySingleton {
private static final HungrySingleton mInstance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return mInstance;
}
}
私有構(gòu)造器是單例的一般套路坎穿,保證不能在外部新建對(duì)象。餓漢式在類加載時(shí)期就已經(jīng)初始化實(shí)例墨林,由于類加載過(guò)程是線程安全的赁酝,所以餓漢式默認(rèn)也是線程安全的。它的缺點(diǎn)也很明顯旭等,我真正需要單例對(duì)象的時(shí)機(jī)是我調(diào)用 getInstance() 的時(shí)候酌呆,而不是類加載時(shí)期。如果單例對(duì)象是很耗資源的搔耕,如數(shù)據(jù)庫(kù)隙袁,socket 等等,無(wú)疑是不合適的弃榨。于是就有了懶漢式菩收。
2、懶漢式
public class LazySingleton {
private static LazySingleton mInstance;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (mInstance == null)
mInstance = new LazySingleton();
return mInstance;
}
}
實(shí)例化的時(shí)機(jī)挪到了 getInstance() 方法中鲸睛,做到了 lazy init 娜饵,但也失去了類加載時(shí)期初始化的線程安全保障。因此使用了 synchronized 關(guān)鍵字來(lái)保障線程安全官辈。但這顯然是一個(gè)無(wú)差別攻擊箱舞,管你要不要同步遍坟,管你是不是多線程,一律給我加鎖晴股。這也帶來(lái)了額外的性能消耗愿伴。這點(diǎn)問(wèn)題肯定難不倒程序員們,于是电湘,雙重檢查鎖定(DCL, Double Check Lock) 應(yīng)運(yùn)而生隔节。
3、DCL
public class DCLSingleton {
private static DCLSingleton mInstance;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (mInstance == null) { // 1
synchronized (DCLSingleton.class) { // 2
if (mInstance == null) // 3
mInstance = new DCLSingleton(); // 4
}
}
return mInstance;
}
}
1 處做第一次判斷寂呛,如果已經(jīng)實(shí)例化了怎诫,直接返回對(duì)象,避免無(wú)用的同步消耗昧谊。2 處僅對(duì)實(shí)例化過(guò)程做同步操作刽虹,保證單例。3 處做第二次判斷呢诬,只有 mInstance 為空時(shí)再初始化∨昼停看起來(lái)時(shí)多么的完美尚镰,保證線程安全的同時(shí)又兼顧性能。但是 DCL 存在一個(gè)致命缺陷哪廓,就是重排序?qū)е碌亩嗑€程訪問(wèn)可能獲得一個(gè)未初始化的對(duì)象狗唉。
首先記住上面標(biāo)記的 4 行代碼。其中第 4 行代碼 mInstance = new DCLSingleton(); 在 JVM 看來(lái)有這么幾步:
1. 為對(duì)象分配內(nèi)存空間
2. 初始化對(duì)象
3. 將 mInstance 引用指向第 1 步中分配的內(nèi)存地址
在單線程內(nèi)涡真,在不影響執(zhí)行結(jié)果的前提下分俯,可能存在指令重排序。例如下列代碼:
int a = 1;
int b = 2;
在 JVM 中你是無(wú)法確保這兩行代碼誰(shuí)先執(zhí)行的哆料,因?yàn)檎l(shuí)先執(zhí)行都不影響程序運(yùn)行結(jié)果缸剪。同理,創(chuàng)建實(shí)例對(duì)象的三部中东亦,第 2 步 初始化對(duì)象 和 第 3 步 將 mInstance 引用指向?qū)ο蟮膬?nèi)存地址 之間也是可能存在重排序的杏节。
為對(duì)象分配內(nèi)存空間
將 mInstance 引用指向第 1 步中分配的內(nèi)存地址
初始化對(duì)象
這樣的話,就存在這樣一種可能典阵。線程 A 按上面重排序之后的指令執(zhí)行奋渔,當(dāng)執(zhí)行到第 2 行 將 mInstance 引用指向?qū)ο蟮膬?nèi)存地址 時(shí),線程 B 開(kāi)始執(zhí)行了壮啊,此時(shí)線程 A 已為 mInstance 賦值嫉鲸,線程 B 進(jìn)行 DCL 的第一次判斷 if (mInstance == null) ,結(jié)果為 false,直接返回 mInstance 指向的對(duì)象歹啼,但是由于重排序的緣故玄渗,對(duì)象其實(shí)尚未初始化减江,這樣就出問(wèn)題了。還挺繞口的捻爷,借用 《Java 并發(fā)編程藝術(shù)》 中的一張表格辈灼,會(huì)對(duì)執(zhí)行流程更加清晰。
時(shí)間 | 線程 A | 線程 B |
---|---|---|
t1 | A1: 分配對(duì)象的內(nèi)存空間 | |
t2 | A3: 設(shè)置 mInstance 指向內(nèi)存空間 | |
t3 | B1: 判斷 mInstance 是否為空 | |
t4 | B2: 由于 mInstance 不為空也榄,線程 B 將訪問(wèn) mInstance 指向的對(duì)象 | |
t5 | A2: 初始化對(duì)象 | |
t6 | A3: 訪問(wèn) mInstance 引用的對(duì)象 | A3 和 A2 發(fā)生重排序?qū)е戮€程 B 獲取了一個(gè)尚未初始化的對(duì)象巡莹。 |
說(shuō)了半天,該怎么改甜紫?其實(shí)很簡(jiǎn)單降宅,禁止多線程下的重排序就可以了,只需要用 volatile 關(guān)鍵字修飾 mInstance 囚霸。在 JDK 1.5 中腰根,增強(qiáng)了 volatile 的內(nèi)存語(yǔ)義,對(duì)一個(gè)volatile 域的寫拓型,happens-before 于任意后續(xù)對(duì)這個(gè) volatile 域的讀额嘿。volatile 會(huì)禁止一些處理器重排序,此時(shí) DCL 就做到了真正的線程安全劣挫。
4册养、靜態(tài)內(nèi)部類模式
public class StaticInnerSingleton {
private StaticInnerSingleton(){}
private static class SingletonHolder{
private static final StaticInnerSingleton mInstance=new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance(){
return SingletonHolder.mInstance;
}
}
鑒于 DCL 繁瑣的代碼,程序員又發(fā)明了靜態(tài)內(nèi)部類模式压固,它和餓漢式一樣基于類加載時(shí)器的線程安全球拦,但是又做到了延遲加載。SingletonHolder 是一個(gè)靜態(tài)內(nèi)部類帐我,當(dāng)外部類被加載的時(shí)候并不會(huì)初始化坎炼。當(dāng)調(diào)用 getInstance() 方法時(shí),才會(huì)被加載拦键。
枚舉單例暫且不提谣光,放在最后再說(shuō)。先對(duì)上面的單例模式做個(gè)檢測(cè)矿咕。
二抢肛、真的是單例?
還記得開(kāi)頭的提問(wèn)嗎碳柱?
你的單例線程安全嗎?
你的單例反射安全嗎捡絮?
你的單例序列化安全嗎?
上面大篇幅的論述都在說(shuō)明線程安全莲镣。下面看看反射安全和序列化安全福稳。
1、反射安全
直接上代碼瑞侮,我用 DCL 來(lái)做測(cè)試:
public static void main(String[] args) {
DCLSingleton singleton1 = DCLSingleton.getInstance();
DCLSingleton singleton2 = null;
try {
Class<DCLSingleton> clazz = DCLSingleton.class;
Constructor<DCLSingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
singleton2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
執(zhí)行結(jié)果:
1627674070
1360875712
很無(wú)情的圆,通過(guò)反射破壞了單例鼓拧。如何保證反射安全呢?只能以暴制暴越妈,當(dāng)已經(jīng)存在實(shí)例的時(shí)候再去調(diào)用構(gòu)造函數(shù)直接拋出異常季俩,對(duì)構(gòu)造函數(shù)做如下修改:
private DCLSingleton() {
if (mInstance!=null)
throw new RuntimeException("想反射我,沒(méi)門梅掠!");
}
上面的測(cè)試代碼會(huì)直接拋出異常酌住。
2、序列化安全
將你的單例類實(shí)現(xiàn) Serializable 持久化保存起來(lái)阎抒,日后再恢復(fù)出來(lái)酪我,他還是單例嗎?
public static void main(String[] args) {
DCLSingleton singleton1 = DCLSingleton.getInstance();
DCLSingleton singleton2 = null;
try {
ObjectOutput output=new ObjectOutputStream(new FileOutputStream("singleton.ser"));
output.writeObject(singleton1);
output.close();
ObjectInput input=new ObjectInputStream(new FileInputStream("singleton.ser"));
singleton2= (DCLSingleton) input.readObject();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
執(zhí)行結(jié)果:
644117698
793589513
不堪一擊且叁。反序列化時(shí)生成了新的實(shí)例對(duì)象都哭。要修復(fù)也很簡(jiǎn)單,只需要修改反序列化的邏輯就可以了逞带,即重寫 readResolve() 方法欺矫,使其返回統(tǒng)一實(shí)例。
protected Object readResolve() {
return getInstance();
}
脆弱不堪的單例模式經(jīng)過(guò)重重考驗(yàn)掰担,進(jìn)化成了完全體汇陆,延遲加載,線程安全带饱,反射安全,序列化安全阅羹。全部代碼如下:
public class DCLSingleton implements Serializable {
private static DCLSingleton mInstance;
private DCLSingleton() {
if (mInstance!=null)
throw new RuntimeException("想反射我勺疼,沒(méi)門!");
}
public static DCLSingleton getInstance() {
if (mInstance == null) {
synchronized (DCLSingleton.class) {
if (mInstance == null)
mInstance = new DCLSingleton();
}
}
return mInstance;
}
protected Object readResolve() {
return getInstance();
}
}
三捏鱼、枚舉單例
在介紹利用枚舉實(shí)現(xiàn)單例模式的原理执庐,先介紹一些相關(guān)的基礎(chǔ)內(nèi)容。
首先导梆,枚舉類似類轨淌,一個(gè)枚舉可以擁有成員變量,成員方法看尼,構(gòu)造方法递鹉。先來(lái)看枚舉最基本的用法:
enum Type{
A,B,C,D;
}
創(chuàng)建enum時(shí),編譯器會(huì)自動(dòng)為我們生成一個(gè)繼承自java.lang.Enum的類藏斩,我們上面的enum可以簡(jiǎn)單看作:
class Type extends Enum{
public static final Type A;
public static final Type B;
...
}
對(duì)于上面的例子躏结,我們可以把Type看作一個(gè)類,而把A狰域,B媳拴,C黄橘,D看作類的Type的實(shí)例。
當(dāng)然屈溉,這個(gè)構(gòu)建實(shí)例的過(guò)程不是我們做的塞关,一個(gè)enum的構(gòu)造方法限制是private的,也就是不允許我們調(diào)用子巾。
“類”方法和“實(shí)例”方法
上面說(shuō)到帆赢,我們可以把Type看作一個(gè)類,而把A砰左,B匿醒。。缠导×幔看作Type的一個(gè)實(shí)例。同樣僻造,在enum中憋他,我們可以定義類和實(shí)例的變量以及方法∷柘鳎看下面的代碼:
enum Type{
A,B,C,D;
static int value;
public static int getValue() {
return value;
}
String type;
public String getType() {
return type;
}
}
在原有的基礎(chǔ)上竹挡,添加了類方法和實(shí)例方法。我們把Type看做一個(gè)類立膛,那么enum中靜態(tài)的域和方法揪罕,都可以視作類方法。和我們調(diào)用普通的靜態(tài)方法一樣宝泵,這里調(diào)用類方法也是通過(guò) Type.getValue()即可調(diào)用好啰,訪問(wèn)類屬性也是通過(guò)Type.value即可訪問(wèn)。
下面的是實(shí)例方法儿奶,也就是每個(gè)實(shí)例才能調(diào)用的方法框往。那么實(shí)例是什么呢?沒(méi)錯(cuò)闯捎,就是A椰弊,B,C瓤鼻,D秉版。所以我們調(diào)用實(shí)例方法,也就通過(guò) Type.A.getType()來(lái)調(diào)用就可以了娱仔。
最后沐飘,對(duì)于某個(gè)實(shí)例而言,還可以實(shí)現(xiàn)自己的實(shí)例方法。再看下下面的代碼:
enum Type{
A{
public String getType() {
return "I will not tell you";
}
},B,C,D;
static int value;
public static int getValue() {
return value;
}
String type;
public String getType() {
return type;
}
}
這里耐朴,A實(shí)例后面的{…}就是屬于A的實(shí)例方法借卧,可以通過(guò)覆蓋原本的方法,實(shí)現(xiàn)屬于自己的定制筛峭。
除此之外铐刘,我們還可以添加抽象方法在enum中,強(qiáng)制ABCD都實(shí)現(xiàn)各自的處理邏輯:
enum Type{
A{
public String getType() {
return "A";
}
},B {
@Override
public String getType() {
return "B";
}
},C {
@Override
public String getType() {
return "C";
}
},D {
@Override
public String getType() {
return "D";
}
};
public abstract String getType();
}
枚舉單例
有了上面的基礎(chǔ)影晓,我們可以來(lái)看一下枚舉單例的實(shí)現(xiàn)方法:
public enum SomeThing {
INSTANCE;
public String get(){ return "SomeThing";}
}
main(){
SomeThing something = SomeThing.INSTANCE;
someThing.get();
}
上面的類Resource是我們要應(yīng)用單例模式的資源镰吵,具體可以表現(xiàn)為網(wǎng)絡(luò)連接,數(shù)據(jù)庫(kù)連接挂签,線程池等等疤祭。
獲取資源的方式很簡(jiǎn)單,只要 SomeThing.INSTANCE.getInstance() 即可獲得所要實(shí)例饵婆。下面我們來(lái)看看單例是如何被保證的:
首先勺馆,在枚舉中我們明確了構(gòu)造方法限制為私有,在我們?cè)L問(wèn)枚舉實(shí)例時(shí)會(huì)執(zhí)行構(gòu)造方法侨核,同時(shí)每個(gè)枚舉實(shí)例都是static final類型的草穆,也就表明只能被實(shí)例化一次。在調(diào)用構(gòu)造方法時(shí)搓译,我們的單例被實(shí)例化悲柱。
也就是說(shuō),因?yàn)閑num中的實(shí)例被保證只會(huì)被實(shí)例化一次些己,所以我們的INSTANCE也被保證實(shí)例化一次豌鸡。
可以看到,枚舉實(shí)現(xiàn)單例還是比較簡(jiǎn)單的段标,除此之外我們?cè)賮?lái)看一下Enum這個(gè)類的聲明:
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable
可以看到直颅,枚舉也提供了序列化機(jī)制。某些情況怀樟,比如我們要通過(guò)網(wǎng)絡(luò)傳輸一個(gè)數(shù)據(jù)庫(kù)連接的句柄,會(huì)提供很多幫助盆佣。