設(shè)計模式-單例模式【實現(xiàn)、序列化奋岁、反射】
[toc]
1. 實現(xiàn)
單例模式的實現(xiàn)有很多種思瘟,分類方式也不一而足,比如分為預(yù)加載和懶加載闻伶,以及線程安全的實現(xiàn)及線程不安全的實現(xiàn)
1.1. 線程不安全
1.1.1 餓漢式
調(diào)用時判斷實例是否已經(jīng)初始化滨攻,沒有的話初始化并賦值。
優(yōu)點:
- 懶加載
- 運(yùn)行效率高
缺點:
- 非線程安全:實例未初始化時,如果有多個線程并發(fā)調(diào)用getInstance方法光绕,可能會造成各線程獲取到不同的實例
適用:如非確定不會被多線程調(diào)用更鲁,否則不建議使用
public static PlainNotSafe getInstance() {
if (instance == null) {
instance = new PlainNotSafe();
}
return instance;
}
1.2. 線程安全
1.2.1 飽漢式
在類初始化過程中即進(jìn)行實例的創(chuàng)建:
優(yōu)點:
- 實現(xiàn)簡單
- 線程安全:實例初始化在類加載階段完成,JVM內(nèi)部保證此過程的線程安全性
缺點:
- 非懶加載
適用: 實例初始化耗費資源少奇钞,或者啟動時間不敏感澡为,或者業(yè)務(wù)要求啟動后快速響應(yīng)
class LoadAhead implements Singleton{
private static LoadAhead instance = new LoadAhead();
private LoadAhead(){}
public static LoadAhead getInstance(){
return instance;
}
}
1.2.2 單同步鎖
使用syncronized關(guān)鍵字修飾getInstance方法
public static synchronized LazyLoadWithOneSynchronization getInstance(){
if (instance == null) {
instance = new LazyLoadWithOneSynchronization();
}
return instance;
}
優(yōu)點:
- 線程安全
- 實現(xiàn)簡單
缺點:
- 高并發(fā)環(huán)境,執(zhí)行效率低:同時只有一個線程可以獲取實例
適用: 不適用任何場景
1.2.3 雙重檢查+同步鎖
考慮到實例創(chuàng)建過程僅需要同步一次景埃,后面不需要同步媒至,因此只在實例未創(chuàng)建時進(jìn)行同步:
public static LazyLoadWithDoubleCheckSynchronization getInstance(){
if (instance == null) {
synchronized (LazyLoadWithDoubleCheckSynchronization.class) {
if (instance == null) {
instance = new LazyLoadWithDoubleCheckSynchronization();
}
}
}
return instance;
}
優(yōu)點:
- 線程安全
- 并發(fā)執(zhí)行效率高:僅在實例第一次創(chuàng)建過程有鎖競爭
缺點:
- 實現(xiàn)復(fù)雜
適用:對于不考慮序列化及反射破壞唯一性的場景,推薦使用此方法
1.2.4 內(nèi)部類
通過內(nèi)部類持有唯一實例谷徙,通過類加載機(jī)制保證懶加載和線程安全
/**
* @Author: kkyeer
* @Description: 懶漢式3拒啰,使用內(nèi)部類來進(jìn)行懶加載,原理是內(nèi)部類初始化時完慧,使用
* @Date:Created in 14:57 2019/6/24
* @Modified By:
*/
class LazyLoadWithInnerClass implements Singleton{
private LazyLoadWithInnerClass(){}
private static class Inner{
static LazyLoadWithInnerClass instance = new LazyLoadWithInnerClass();
}
public static LazyLoadWithInnerClass getInstance(){
return Inner.instance;
}
}
優(yōu)點:
- 線程安全
- 懶加載
缺點:
- 實現(xiàn)復(fù)雜
適用:相對上一個雙重檢查谋旦,多一次(或多次)尋址開銷,不推薦使用
1.2.5 枚舉
通過枚舉實現(xiàn)單例屈尼,推薦使用此方式册着,能在多個維度保證安全:
- 線程安全
- 序列化不破壞唯一性
- 反射調(diào)用不破壞唯一性
- 實現(xiàn)簡單
實現(xiàn)如下:
enum LazyLoadWithEnum implements Singleton{
INSTANCE;
Singleton getInstance(){
return INSTANCE;
}
}
2. 其他創(chuàng)建對象方式對單例唯一的破壞
單例模式的核心是,在設(shè)定的上下文中脾歧,指定的類的實例僅有一個甲捏,此處的上下文,根據(jù)需求不同鞭执,可能指JVM司顿、同一SpringContext等,然而兄纺,我們都學(xué)過大溜,創(chuàng)建一個對象有4種方式:
- new關(guān)鍵字:
new Object()
- 對象反序列化:
objectInputStream.readObject()
- 反射調(diào)用:
Object.class.getDeclaredConstructor().newInstance()
- clone方法:
obj.clone()
雖然在上述的單例實現(xiàn)中,已經(jīng)考慮了構(gòu)造器私有化估脆,保證使用者無法通過new一個新對象的方式破壞唯一性钦奋,但仍舊有可能通過其他三種方式,獲取到另外的實例旁蔼,破壞單例模式的唯一性
2.1 clone方法另外創(chuàng)建單例對象破壞單例唯一性
clone方法為Object的方法锨苏,理論上所有的對象都繼承,但是由于此方法為protected方法棺聊,且要求必須顯式的implement Cloneable接口伞租,換句話說,必須本類(或父類)顯式實現(xiàn)clone方法并將之?dāng)U大為public權(quán)限限佩,因此葵诈,clone方法雖然會破壞單例模式的唯一性裸弦,但更多是由于在定義單例類時,override clone方法時造成的錯誤作喘,因此不做討論
2.2 對象反序列化破壞單例唯一性
對于非Enum的單例實現(xiàn)來說理疙,對象反序列化能破壞單例模式的唯一性:
private static void testSerialization(){
Singleton created = LazyLoadWithInnerClass.getInstance();
System.out.println(created.hashCode());
File testFile = new File("obj.txt");
try {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(testFile));
objectOutputStream.writeObject(created);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(testFile));
Singleton dematerializedObject = (Singleton) objectInputStream.readObject();
objectInputStream.close();
System.out.println(dematerializedObject.hashCode());
Assertions.assertTrue(dematerializedObject == created,"破壞了單例唯一性");
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
testFile.delete();
}
}
運(yùn)行結(jié)果:
1995265320
1880587981
Exception in thread "main" java.lang.AssertionError: 破壞了單例唯一性
at utils.Assertions.assertTrue(Assertions.java:27)
at design.pattern.singleton.TestCase.testSerialization(TestCase.java:116)
at design.pattern.singleton.TestCase.main(TestCase.java:25)
2.2.1 源碼解析
ObjectInputStream.readObject方法內(nèi)部,會判斷要反序列化的對象的類型泞坦,對于普通對象(非String, Class,* ObjectStreamClass, array, or enum constant),調(diào)用下列方法來反序列化:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
// 略
// ↓↓↓↓↓↓↓↓↓初始化新實例↓↓↓↓↓↓↓↓↓↓
obj = desc.isInstantiable() ? desc.newInstance() : null;
// 略
// ↓↓↓↓↓↓↓↓↓調(diào)用readResolve方法覆蓋↓↓↓↓↓↓↓↓↓↓
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
// 略
if (rep != obj) {
// 略
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
過程為:
-
對于可以實例化(調(diào)用public無參構(gòu)造器)的對象窖贤,調(diào)用ObjectStreamClass的newInstance方法:
Object newInstance() throws InstantiationException, InvocationTargetException, UnsupportedOperationException { // 略 return cons.newInstance(); // 略 }
變量cons為
private Constructor<?> cons;
,忽略安全檢查部分,實際上通過反射來創(chuàng)建新的實例對象贰锁,如果將此新創(chuàng)建的對象作為最終結(jié)果赃梧,則破壞了單例的唯一性 -
如果目標(biāo)類實現(xiàn)了readResolve方法,則調(diào)用readResolve方法豌熄,并用返回的結(jié)果覆蓋上一步的結(jié)果授嘀,因此,一種避免序列化破壞單例唯一性的思路即手動實現(xiàn)readResolve方法:
private Object readResolve(){ return Inner.instance; }
2.3 反射調(diào)用破壞單例的唯一性
與上述反序列化的源碼解析類似锣险,直接通過class對象的newInstance方法或者通過獲取其Constructor對象并調(diào)用來創(chuàng)建實例時蹄皱,也會重新生成一個新的實例,從而破壞單例的唯一性芯肤,當(dāng)然巷折,通過在構(gòu)造器中維護(hù)一個flag變量,在多次構(gòu)造時拋出異常可以(一定程度上)避免此問題:
private static boolean initFlag = false;
private LazyLoadWithDoubleCheckSynchronization(){
if (initFlag) {
throw new RuntimeException("多次嘗試調(diào)用構(gòu)造函數(shù)纷妆,破壞單例的唯一性");
}
initFlag = true;
// 其他構(gòu)造過程
}
2.4 使用枚舉避免序列化和反射過程中對單例的破壞
使用枚舉來實現(xiàn)單例模式盔几,可以防止序列化和反射過程中對單例的破壞
2.4.1 單例模式避免序列化過程中對單例唯一性的破壞
對于單例的反序列化晴弃,在從流解析對象過程中掩幢,調(diào)用如下方法:
private Enum<?> readEnum(boolean unshared) throws IOException {
// 略
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
// 略
}
return result;
}
可見流存儲的僅為枚舉的name,反序列化時上鞠,根據(jù)name际邻,調(diào)用Enum的valueOf方法,獲取JVM已經(jīng)初始化的實例芍阎,因此世曾,單例模式使用枚舉實現(xiàn),可以保證反序列化不破壞單例的唯一性
2.4.2 單例模式避免反射破壞單例唯一性
枚舉類無法進(jìn)行反射調(diào)用谴咸,實際考慮使用下面的代碼嘗試進(jìn)行反射創(chuàng)建枚舉實例
private static void testReflection() {
try {
Singleton created = LazyLoadWithEnum.INSTANCE.getInstance();
Constructor<LazyLoadWithEnum> constructor = LazyLoadWithEnum.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Singleton instanceWithReflection = constructor.newInstance();
System.out.println(created.hashCode());
System.out.println(instanceWithReflection.hashCode());
Assertions.assertTrue(instanceWithReflection == created,"反射破壞單例唯一性");
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
注意轮听,枚舉類的Constructor對象獲取和newInstance方法均不同于普通類
-
枚舉類看似有空構(gòu)造方法,其實并非如此岭佳,下面是使用【DJ Java Decompiler 3.12】反編譯的枚舉對應(yīng)的class文件:
final class LazyLoadWithEnum extends Enum implements Singleton { public static LazyLoadWithEnum[] values() { return (LazyLoadWithEnum[])$VALUES.clone(); } public static LazyLoadWithEnum valueOf(String name) { return (LazyLoadWithEnum)Enum.valueOf(design/pattern/singleton/LazyLoadWithEnum, name); } private LazyLoadWithEnum(String s, int i) { super(s, i); } Singleton getInstance() { return INSTANCE; } public static final LazyLoadWithEnum INSTANCE; private static final LazyLoadWithEnum $VALUES[]; static { INSTANCE = new LazyLoadWithEnum("INSTANCE", 0); $VALUES = (new LazyLoadWithEnum[] { INSTANCE }); } }
觀察發(fā)現(xiàn)血巍,此類未定義無參構(gòu)造器,取而代之的是
private LazyLoadWithEnum(String s, int i)
珊随,因此獲取構(gòu)造器時述寡,應(yīng)指定參數(shù)列表為(String,int) 調(diào)用Constructor的newInstance()方法時柿隙,如果是枚舉類型,會拋出異常:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
// 略
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
// 略
}
因此鲫凶,使用Enum來實現(xiàn)單例禀崖,可以保證不會因為反射調(diào)用來破壞單例的唯一性