單例模式是最常用的設(shè)計(jì)模式之一,不管是工作中,還是面試中,單例模式一直都是寵兒。單例模式看似簡(jiǎn)單,但是如果深入理解單例模式的各種實(shí)現(xiàn),會(huì)涉及到很其他方面的知識(shí)點(diǎn),可研究性非常高,而且面試也會(huì)非常喜歡深入去探討單例模式。接下來(lái)講解一下單例模式的各種實(shí)現(xiàn)、優(yōu)缺點(diǎn)、防御等。
定義:保證一個(gè)類只有一個(gè)實(shí)例,并提供一個(gè)全局訪問(wèn)點(diǎn)
類型:創(chuàng)建型
適用場(chǎng)景:想要確保任何情況下都絕對(duì)只有一個(gè)實(shí)例
優(yōu)點(diǎn):在內(nèi)存里只有一個(gè)實(shí)例,減少了內(nèi)存開(kāi)銷;可以避免對(duì)資源的多重占用;設(shè)置全局訪問(wèn)點(diǎn),嚴(yán)格控制訪問(wèn)
缺點(diǎn):沒(méi)有接口,擴(kuò)展困難
重點(diǎn):
- 私有構(gòu)造器
- 線程安全
- 延遲加載
- 序列化和反序列安全
- 反射防御
實(shí)現(xiàn)方式
要保證構(gòu)造器是私有的,這點(diǎn)很重要,所有實(shí)現(xiàn)方式都要遵循這一點(diǎn)。
懶漢模式
懶漢模式即讓實(shí)例延時(shí)加載,在需要的時(shí)候才去創(chuàng)建實(shí)例,減少資源占用
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
實(shí)例對(duì)象在第一次調(diào)用getInstance方法獲取時(shí)才會(huì)創(chuàng)建,這就是懶加載的思想。但是這種寫(xiě)法在單線程下是沒(méi)問(wèn)題的,但是在多線程下并發(fā)地第一次訪問(wèn)這個(gè)方法會(huì)出現(xiàn)線程安全問(wèn)題,導(dǎo)致創(chuàng)建多個(gè)實(shí)例,優(yōu)化方式就是給獲取對(duì)象的方法加鎖
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
雙重檢查DoubleCheck
在懶漢模式中,為了解決多線程安全問(wèn)題,給整個(gè)方法都加了鎖,但是效率可以改進(jìn),DoubleCheck寫(xiě)法就是對(duì)懶漢模式的改進(jìn)
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
DoubleCheck寫(xiě)法沒(méi)有鎖住整個(gè)方法,而是鎖住了創(chuàng)建對(duì)象的那部分代碼,減小鎖范圍,增加了效率,且因?yàn)樽隽藘纱慰张袛?,所以得名雙重檢查。
但是這樣寫(xiě)法有一個(gè)指令重排序的問(wèn)題,就是 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton() 這段代碼在JVM執(zhí)行的時(shí)候?qū)嶋H上被拆分為三步,其中2、3兩個(gè)步驟有可能會(huì)被重排序,即3在前,2在后
- 給對(duì)象分配內(nèi)存空間
- 初始化對(duì)象
- 設(shè)置 lazyDoubleCheckSingleton 指向剛剛分配的內(nèi)存地址
as-if-seria語(yǔ)義:為了提高編譯器和處理器的執(zhí)行速度,如果在單線程下程序的結(jié)果不會(huì)被改變,那么就允許對(duì)指令進(jìn)行重排序。
上面說(shuō)的三個(gè)步驟,在單線程下,2、3兩個(gè)步驟即使調(diào)換順序,也不影響使用,所以是被允許重排序的。
但是如果是多線程下,發(fā)生了重排序就會(huì)出現(xiàn)問(wèn)題。比如線程一執(zhí)行到了 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton() 這段代碼,并且發(fā)生了重排序,先執(zhí)行1、3兩個(gè)步驟,還未進(jìn)行初始化。然后此時(shí)線程二執(zhí)行到了第一個(gè)檢查 if(lazyDoubleCheckSingleton == null) ,因?yàn)?lazyDoubleCheckSingleton 已經(jīng)被線程一指向了內(nèi)存地址,所以不為null,所以線程二就直接返回了lazyDoubleCheckSingleton。并且線程二直接就開(kāi)始使用lazyDoubleCheckSingleton對(duì)象了,然而此時(shí)lazyDoubleCheckSingleton還沒(méi)有被初始化,所以線程二直接適用就會(huì)出現(xiàn)異常。
解決辦法就是給lazyDoubleCheckSingleton對(duì)象增加volatile關(guān)鍵字
public class LazyDoubleCheckSingleton {
private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
volatile 可以禁止指令重排序,所以不會(huì)出現(xiàn)這個(gè)問(wèn)題,如果用DoubleCheck寫(xiě)法實(shí)現(xiàn)單例模式,實(shí)例不加上volatile關(guān)鍵字,一切涼涼。volatile關(guān)鍵字這里不細(xì)講了。
所以在使用懶加載的單例模式時(shí),上面這種寫(xiě)法是最全的,而由于涉及到的知識(shí)面較多,所以這種寫(xiě)法非常適合在面試中小露一手。
靜態(tài)內(nèi)部類實(shí)現(xiàn)
基于類初始化的延遲加載解決方案
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){}
private static class SingletonHelper{
private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return SingletonHelper.singleton;
}
}
SingletonHelper是私有內(nèi)部類,外部無(wú)法訪問(wèn)。這種形式在 StaticInnerClassSingleton 類被加載的時(shí)候,內(nèi)部類還不會(huì)被加載,所以內(nèi)部類的實(shí)例對(duì)象還沒(méi)被加載,在調(diào)用getInstance時(shí)候才會(huì)加載SingletonHelper類,從而完成singleton 實(shí)例的創(chuàng)建,從而達(dá)到了延遲加載的目的。
惡漢模式
類加載的時(shí)候就完成實(shí)例化,實(shí)現(xiàn)比較簡(jiǎn)單,缺點(diǎn)就是如果不使用該實(shí)例,就會(huì)造成資源的浪費(fèi)
public class HungrySingleton {
private HungrySingleton(){}
private static final HungrySingleton singleton = new HungrySingleton();
public static HungrySingleton getInstance(){
return singleton;
}
}
還有一種變種,將對(duì)象的實(shí)例化移到靜態(tài)代碼塊中
public class HungrySingleton {
private HungrySingleton(){}
private final static HungrySingleton singleton;
static{
singleton = new HungrySingleton();
}
public static HungrySingleton getInstance(){
return singleton;
}
}
這樣也是在類加載的時(shí)候就會(huì)完成實(shí)例化
破壞單例模式
單例模式的定義就是全局只有一個(gè)實(shí)例,那么想要破壞單例模式,就是創(chuàng)建一個(gè)新的實(shí)例即可。那么創(chuàng)建實(shí)例有哪些方法呢?
序列化與反序列化破壞單例模式
通過(guò)反序列化創(chuàng)建一個(gè)新的實(shí)例,這里使用惡漢模式做測(cè)試,用哪種實(shí)現(xiàn)方式都行,注意給單例類實(shí)現(xiàn) Serializable
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton singleton = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test_data"));
oos.writeObject(singleton);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test_data"));
HungrySingleton newSingleton = (HungrySingleton) ois.readObject();
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
}
此時(shí)打印出來(lái)的結(jié)果是
design_pattern.singleton.HungrySingleton@4b67cf4d
design_pattern.singleton.HungrySingleton@16b98e56
false
可以看到兩個(gè)實(shí)例不一樣,破壞了單例模式的全局唯一性。
解決方案如下,在單例類里面增加一個(gè) readResolve() 方法,返回唯一的那個(gè)實(shí)例
public class HungrySingleton implements Serializable {
private HungrySingleton(){}
private static final HungrySingleton singleton = new HungrySingleton();
public static HungrySingleton getInstance(){
return singleton;
}
// 增加該方法
private Object readResolve(){
return singleton;
}
}
這時(shí)候再執(zhí)行測(cè)試代碼,結(jié)果兩個(gè)實(shí)例就是一致的
design_pattern.singleton.HungrySingleton@4b67cf4d
design_pattern.singleton.HungrySingleton@4b67cf4d
true
反序列化的時(shí)候原本是創(chuàng)建一個(gè)新的實(shí)例,但是如果增加了這個(gè)方法,就會(huì)反射調(diào)用該方法,替換掉原本創(chuàng)建的那個(gè)實(shí)例,實(shí)際上是有創(chuàng)建了新的實(shí)例,但是沒(méi)有被返回。源碼就不細(xì)講了。
當(dāng)單例類有涉及到序列化與反序列化時(shí)一定要注意反序列化對(duì)單例模式的破壞。
反射攻擊破壞單例模式
利用反射創(chuàng)建新的實(shí)例,雖然構(gòu)造方法已經(jīng)設(shè)為private,但是反射依舊可以修改訪問(wèn)權(quán)限,這里依然使用惡漢模式做一下測(cè)試
HungrySingleton instance = HungrySingleton.getInstance();
Class clz = HungrySingleton.class;
Constructor constructor = clz.getDeclaredConstructor();
constructor.setAccessible(true); // 打開(kāi)構(gòu)造方法的訪問(wèn)權(quán)限
Object newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
打印出來(lái)的結(jié)果如下:
design_pattern.singleton.HungrySingleton@6d6f6e28
design_pattern.singleton.HungrySingleton@135fbaa4
false
可以看到已經(jīng)出現(xiàn)了兩個(gè)實(shí)例,破壞了單例模式。
那有沒(méi)有辦法防御這種破壞呢?在惡漢模式中,實(shí)例只會(huì)在類加載的時(shí)候被創(chuàng)建,后面在使用單例類的時(shí)候,實(shí)際上實(shí)例已經(jīng)存在了。那么我們可以在構(gòu)造方法中直接拋出異常,阻止用構(gòu)造方法創(chuàng)建實(shí)例。
public class HungrySingleton{
private HungrySingleton(){
if(singleton != null){
throw new RuntimeException("禁止用構(gòu)造器創(chuàng)建實(shí)例");
}
}
private static final HungrySingleton singleton = new HungrySingleton();
public static HungrySingleton getInstance(){
return singleton;
}
}
再次執(zhí)行測(cè)試類,就會(huì)拋出異常。靜態(tài)內(nèi)部類實(shí)現(xiàn)方式也可以用這種方式防御。
但是在懶加載的模式下,例如 LazySingleton 單例類被加載的時(shí)候,singleton 實(shí)例是還沒(méi)有被初始化的,在構(gòu)造方法中拋異常這種方法是無(wú)法做到真正預(yù)防,如果是先調(diào)用 LazySingleton.getInstance() 創(chuàng)建實(shí)例,再反射調(diào)用構(gòu)造方法是可行的,因?yàn)檎{(diào)用完 LazySingleton.getInstance() 方法之后,實(shí)例就被創(chuàng)建了,此時(shí)構(gòu)造方法就會(huì)拋異常。但是如果先用反射調(diào)用構(gòu)造方法創(chuàng)建實(shí)例的話就無(wú)法預(yù)防到了。
所以懶加載實(shí)現(xiàn)的單例模式還是存在安全隱患的。
字節(jié)碼技術(shù)破壞單例模式
如果用字節(jié)碼技術(shù)可以動(dòng)態(tài)修改字節(jié)碼,也就是class文件,想怎么改就怎么改,這種方式應(yīng)該是沒(méi)法防御的。。。
枚舉實(shí)現(xiàn)單例模式
《Effective Java》中推薦使用枚舉的方式來(lái)實(shí)現(xiàn)單例模式
public enum EnumInstance {
INSTANCE{
@Override
public void print() {
System.out.println("用枚舉實(shí)現(xiàn)單例模式");
}
};
private Object data;
public abstract void print();
public Object getData() { return data; }
public void setData(Object data) { this.data = data; }
}
這種實(shí)現(xiàn)方式符合全局唯一性,且無(wú)懼 序列化與反序列化 和 反射 的攻擊。
參考:
慕課網(wǎng) Geely 老師的《Java設(shè)計(jì)模式精講 Debug方式+內(nèi)存分析》