@[toc]
前面我們介紹了單例模式的餓漢式和懶漢式寫法荚板,以及從最簡陋的懶漢式到 DCL 版本的演進杰扫,相信你對單例模式已經(jīng)有了很深刻的認識队寇。這一章節(jié)將繼續(xù)介紹另外兩種單例模式的寫法——靜態(tài)內(nèi)部類和枚舉類單例,在介紹完成后從底層代碼剖析這兩種寫法的優(yōu)勢和原理章姓。最后便是單例模式在 JDK 和其他框架下的的源碼以及應用佳遣。
1. 使用靜態(tài)內(nèi)部類實現(xiàn)單例模式
1.1 靜態(tài)內(nèi)部類單例寫法
前面介紹了餓漢式的單例模式確保了線程安全,但是不能夠?qū)崿F(xiàn)延遲加載凡伊;懶漢式能夠確保延遲加載零渐,卻需要確保線程安全。有沒有一種辦法既能夠?qū)崿F(xiàn)延遲加載系忙,又不需要使用同步代碼就能夠保證線程安全的單例呢诵盼?答案是有的,使用靜態(tài)內(nèi)部類的方式來實現(xiàn)單例模式银还。代碼如下:
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE= new Singleton();
}
}
我們這里使用靜態(tài)內(nèi)部類 SingletonHolder风宁,并將單例成員變量移到該靜態(tài)內(nèi)部類中,獲取單例時直接調(diào)用 SingletonHolder.INSTANCE便可以獲取到該單例蛹疯。靜態(tài)內(nèi)部類與餓漢式的區(qū)別就在于使用了靜態(tài)內(nèi)部類維護對象成員戒财,那么為什么這樣的小改動就能夠即實現(xiàn)懶加載,又是線程安全的呢捺弦?接下來我們對這段代碼進行分析
1.2 如何實現(xiàn)懶加載
首先分析為什么能夠?qū)崿F(xiàn)懶加載饮寞,以下面代碼為例孝扛,Outer 類中有靜態(tài)內(nèi)部類 Inner
public class Outer {
public static final Outer outer = new Outer();
static {
System.out.println("outer static running.");
}
public static class Inner {
public static final Inner inner = new Inner();
static {
System.out.println("inner static running.");
}
}
}
當我們創(chuàng)建一個內(nèi)部類之后,對該類進行編譯之后將會生成兩個 class 文件 Outer.class 和 Outer$Inner.class 幽崩。也就是說當我進行類加載時實際上需要加載兩個類苦始,下面演示兩種情況:只調(diào)用 outer 對象、只調(diào)用 inner 對象慌申。
// 只調(diào)用 outer 對象
public static void main(String[] args) {
Outer outer = Outer.outer;
// 控制臺打印
// outer static running.
}
// 只調(diào)用 inner 對象
public static void main(String[] args) {
Outer.Inner inner = Outer.Inner.inner;
// 控制臺打印
// inner static running.
}
JVM 中類初始化有這么一個規(guī)定:
遇到new陌选、getstatic、putstatic太示、invokestatic這四條字節(jié)碼指令時柠贤,假設類還沒有進行過初始化。則須要先觸發(fā)其初始化类缤。生成這四條指令最常見的Java代碼場景是:使用new關鍵字實例化對象時臼勉、讀取或設置一個類的靜態(tài)字段(static)時(被static修飾又被final修飾的,已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)餐弱、以及調(diào)用一個類的靜態(tài)方法時
因此從上面可以得出以下結(jié)論:只調(diào)用外部類并且不使用與內(nèi)部類相關的成員變量宴霸、方法時,不會對內(nèi)部類進行初始化膏蚓。而根據(jù) JVM 的規(guī)定瓢谢,當我們在外部內(nèi)調(diào)用內(nèi)部類的成員或方法時才會初始化內(nèi)部類,并且只初始化一次驮瞧。
所以從這里可以看出氓扛,在我們不對內(nèi)部類的靜態(tài)成員、靜態(tài)方法進行調(diào)用時內(nèi)部類時不會進行初始化的论笔。而在內(nèi)部類的單例模式中采郎,在外部類調(diào)用了內(nèi)部類的靜態(tài)成員變量 INSTANCE ,從而觸發(fā)類初始化狂魔,因此確保了懶加載機制蒜埋。
1.3 為什么線程安全
分析了懶加載原因之后再看線程安全就比較簡單了。在對內(nèi)部類進行調(diào)用是內(nèi)部類才會初始化最楷,那么此時和餓漢式一樣會先對靜態(tài)成員進行初始化整份,然后再執(zhí)行調(diào)用方法,在類加載時期完成了單例對象的創(chuàng)建籽孙,因此在獲取的時候就不存在線程安全的問題了烈评。
2. 枚舉類型單例單例模式
2.1 枚舉類型單例寫法
在 《Effective Java》 這本書中推薦使用枚舉類型來獲取單例對象,寫法也非常簡單:
public enum Singleton {
INSTANCE;
public Singleton getInstance() {
return INSTANCE;
}
}
2.2 枚舉類型單例原理
那么為什么一個簡單的枚舉就能夠保證線程安全的單例呢犯建?我們反編譯一下這段代碼看看編譯之后的類及成員是什么樣的(javap -p Singleton.class)
Compiled from "Singleton.java"
public final class com.sk.demo.singleton.Singleton extends java.lang.Enum<com.sk.demo.singleton.Singleton> {
// 靜態(tài)成員變量
public static final com.sk.demo.singleton.Singleton INSTANCE;
private static final com.sk.demo.singleton.Singleton[] $VALUES;
public static com.sk.demo.singleton.Singleton[] values();
public static com.sk.demo.singleton.Singleton valueOf(java.lang.String);
// 私有構造方法
private com.sk.demo.singleton.Singleton();
public com.sk.demo.singleton.Singleton getInstance();
static {};
}
可以看到 enum 類在編譯之后轉(zhuǎn)化成了一個 final 類础倍,并繼承 java.lang.Enum 這個抽象類。在編譯之后的 Singleton 類中胎挎,擁有一個靜態(tài)成員變量 INSTANCE沟启,以及私有構造方法。然后我們看看完整的反編譯(javap -c Singleton.class):
Compiled from "Singleton.java"
public final class com.sk.demo.singleton.Singleton extends java.lang.Enum<com.sk.demo.singleton.Singleton> {
public static final com.sk.demo.singleton.Singleton INSTANCE;
public static com.sk.demo.singleton.Singleton[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lcom/sk/demo/singleton/Singleton;
3: invokevirtual #2 // Method "[Lcom/sk/demo/singleton/Singleton;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lcom/sk/demo/singleton/Singleton;"
9: areturn
public static com.sk.demo.singleton.Singleton valueOf(java.lang.String);
Code:
0: ldc #4 // class com/sk/demo/singleton/Singleton
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/En
um;
6: checkcast #4 // class com/sk/demo/singleton/Singleton
9: areturn
public com.sk.demo.singleton.Singleton getInstance();
Code:
0: getstatic #7 // Field INSTANCE:Lcom/sk/demo/singleton/Singleton;
3: areturn
static {};
Code:
0: new #4 // class com/sk/demo/singleton/Singleton
3: dup
4: ldc #8 // String INSTANCE
6: iconst_0
7: invokespecial #9 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #7 // Field INSTANCE:Lcom/sk/demo/singleton/Singleton;
13: iconst_1
14: anewarray #4 // class com/sk/demo/singleton/Singleton
17: dup
18: iconst_0
19: getstatic #7 // Field INSTANCE:Lcom/sk/demo/singleton/Singleton;
22: aastore
23: putstatic #1 // Field $VALUES:[Lcom/sk/demo/singleton/Singleton;
26: return
}
從以上反編譯后的指令可以看到在 static{} 中犹菇,對靜態(tài)變量 INSTANCE 進行構造初始化德迹,從反編譯后的代碼分析就能夠看出 enum 對象編譯之后的類使用餓漢式來保證的單例。
2.3 枚舉類型單例模式的優(yōu)勢
相比于前面的幾種方式揭芍,枚舉類型還有一個好處就是能夠防止反射導致單例失效胳搞。前面幾種辦法都是基于普通類來進行創(chuàng)建、獲取單例對象称杨,若要防止反射破壞單例肌毅,需要單獨進行處理。而 Java 規(guī)定反射不能夠破壞枚舉類型姑原,因此即使使用反射也無法破壞枚舉類型悬而,詳見 java.lang.reflect.Constructor 中的 newInstance 方法。因此枚舉類型的單例是目前最為完美的單例模式寫法了锭汛。
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
// 通過反射類不能夠構造枚舉對象
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
3. 單例模式在源碼中的應用
3.1 JDK 中的單例模式
Unsafe 類
在研究多線程時會經(jīng)常到這個類來笨奠,因為 CAS 就是通過 Unsafe 類來實現(xiàn)的。在 Unsafe 類中唤殴,Unsafe 對象也是通過單例模式獲取般婆。下面從源碼中省略多余代碼,提取出來單例模式部分朵逝∥蹬郏可以看到 Unsafe 構造方法被標記為 private,使用靜態(tài)成員變量 theUnsafe 聲明單例對象配名,并在靜態(tài)代碼塊中進行初始化啤咽,從這里可以看出這是一個標準的餓漢式單例。
public final class Unsafe {
private static final Unsafe theUnsafe;
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
static {
registerNatives();
Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
theUnsafe = new Unsafe();
// 省略多余代碼
}
Runtime 類
同樣的段誊,再看 Runtime 類也是一個標準的餓漢式單例
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
3.2 Spring 中的單例模式
Spring 的 bean 默認就是單例的對象闰蚕,但是在 Spring 中是通過 ConcurrentHashMap 存放對象,并使用三級緩存來確保單例连舍,雖然與我們所講的單例模式都不太一樣没陡,但是從效果和意義上來講這也是單例模式。Spring 對 Bean 的管理可以參考以下文章:
3.3 slf4j 中的單例模式
在 slf4j 中的 LoggerFactory 類中也使用了單例模式索赏。在該類中通過 getILoggerFactory() 方法獲取 LoggerFactory 對象盼玄,從下面的源碼中可以看到,getILoggerFactory() 方法使用的是 DCL 來獲取的單例對象潜腻。
public final class LoggerFactory {
private LoggerFactory() {
}
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == 0) {
Class var0 = LoggerFactory.class;
synchronized(LoggerFactory.class) {
if (INITIALIZATION_STATE == 0) {
INITIALIZATION_STATE = 1;
performInitialization();
}
}
}
// 省略多余代碼
}
}
在 slf4j 中的 StaticLoggerBinder 類同樣也使用到了單例模式埃儿,從下面源碼中可以看到 StaticLoggerBinder 也是使用的餓漢式單例模式。
public class StaticLoggerBinder implements LoggerFactoryBinder {
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
private StaticLoggerBinder() {
this.defaultLoggerContext.setName("default");
}
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
4. 單例模式總結(jié)
單例模式確保了調(diào)用者獲取到的對象始終是同一個
單例模式有餓漢式融涣、懶漢式(DCL)童番、靜態(tài)內(nèi)部類精钮、枚舉等多種寫法,其中枚舉類型是最完美的
枚舉類型單例是指也是餓漢式剃斧,但是枚舉可以防止反射攻擊
單例模式是非常重要的設計模式轨香,并且從源碼可以看出單例模式的使用也是非常廣泛