一般實現(xiàn)單例模式的幾種思路
懶漢和餓漢
- 餓漢式:在類加載時就完成了初始化,所以類加載比較慢,但獲取對象的速度快。
- 懶漢式:在類加載時不初始化,等到第一次被使用時才初始化。
1.餓漢式
package com.d4c.example;
/**
* 餓漢式
*/
public class SingletonHungry {
private final static SingletonHungry SINGLETON_HUNGRY = new SingletonHungry();
private SingletonHungry() {
}
public static SingletonHungry getInstance() {
return SINGLETON_HUNGRY;
}
}
- 優(yōu)點:在類加載的時候就完成了實例化,避免了多線程的同步問題。
- 缺點:因為類加載時就實例化了,沒有達到Lazy Loading (懶加載) 的效果,如果該實例沒被使用,內(nèi)存就浪費了。
2.懶漢式(同步方法)
package com.d4c.example;
/**
* 懶漢式
*/
public class SingletonLazy {
private static SingletonLazy singletonLazy = null;
private SingletonLazy() {
}
public static synchronized SingletonLazy getInstance() {
if (singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
}
- 優(yōu)點:對getInstance()加了鎖的處理,保證了同一時刻只能有一個線程訪問并獲得實例.
- 缺點:也很明顯,因為synchronized是修飾整個方法,每個線程訪問都要進行同步,而其實這個方法只執(zhí)行一次實例化代碼就夠了,每次都同步方法顯然效率低下,為了改進這種寫法,就有了下面的雙重檢查懶漢式。
3.懶漢式(雙重校驗鎖)
package com.d4c.example;
/**
* 懶漢式(DBL)
* volatile關鍵字修飾,防止指令重排
* Double Check Lock(DCL) 雙重鎖校驗
*/
public class SingletonLazyDBL {
private static volatile SingletonLazyDBL singletonLazy = null;
private SingletonLazyDBL() {
}
public static SingletonLazyDBL getInstance() {
if (singletonLazy == null) {
synchronized (SingletonLazyDBL.class) {
if (singletonLazy == null) {
singletonLazy = new SingletonLazyDBL();
}
}
}
return singletonLazy;
}
}
- 優(yōu)點:用了兩個if判斷,也就是Double-Check,并且同步的不是方法,而是代碼塊,效率較高。
為什么要做兩次判斷呢?這是為了線程安全考慮,還是那個場景,對象還沒實例化,兩個線程A和B同時訪問靜態(tài)方法并同時運行到第一個if判斷語句,這時線程A先進入同步代碼塊中實例化對象,結束之后線程B也進入同步代碼塊,如果沒有第二個if判斷語句,那么線程B也同樣會執(zhí)行實例化對象的操作了。
4.靜態(tài)內(nèi)部類方式
package com.d4c.example;
/**
* 懶漢式(內(nèi)部類方式)
*/
public class SingletonInnerType {
private SingletonInnerType() {
}
private static class SingletonHolder {
public static volatile SingletonInnerType SINGLETON_INNER_TYPE = new SingletonInnerType();
}
public static SingletonInnerType getInstance() {
return SingletonHolder.SINGLETON_INNER_TYPE;
}
}
似乎靜態(tài)內(nèi)部類看起來已經(jīng)是最完美的方法了,其實不是,可能還存在反射攻擊或者反序列化攻擊
public static void main(String[] args) throws Exception {
SingletonInnerType singleton = SingletonInnerType.getInstance();
Constructor<SingletonInnerType> constructor = SingletonInnerType.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonInnerType newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
}
或者引入反序列化后,也不是單例的了。
反序列化須引入依賴(方便操作)
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
public static void main(String[] args) throws Exception {
SingletonInnerType instance = SingletonInnerType.getInstance();
System.out.println("instance = " + instance);
byte[] serialize = SerializationUtils.serialize(instance);
SingletonInnerType newInstance = SerializationUtils.deserialize(serialize);
System.out.println("newInstance = " + newInstance);
System.out.println(instance == newInstance);
}
所以,反射攻擊或者反序列化都導致單例失敗。
解決方法,禁止反射就可以了。
優(yōu)化后
package com.d4c.example;
import java.lang.reflect.Constructor;
/**
* 懶漢式(內(nèi)部類方式)
*/
public class SingletonInnerType {
private SingletonInnerType() {
//反射的情況能防住,序列化,反序列化的方式防不住
if (SingletonHolder.SINGLETON_INNER_TYPE!=null){
throw new RuntimeException("破解錯誤!");
}
}
private static class SingletonHolder {
public static volatile SingletonInnerType SINGLETON_INNER_TYPE = new SingletonInnerType();
}
public static SingletonInnerType getInstance() {
return SingletonHolder.SINGLETON_INNER_TYPE;
}
public static void main(String[] args) throws Exception {
//這種破解能防住
SingletonInnerType singleton = SingletonInnerType.getInstance();
Constructor<SingletonInnerType> constructor = SingletonInnerType.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonInnerType newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
//下面這種防不住
SingletonInnerType instance = SingletonInnerType.getInstance();
System.out.println("instance = " + instance);
byte[] serialize = SerializationUtils.serialize(instance);
SingletonInnerType newInstance = SerializationUtils.deserialize(serialize);
System.out.println("newInstance = " + newInstance);
System.out.println(instance == newInstance);
}
}
- 優(yōu)點:線程安全,調用效率高,可以延時加載
這是很多開發(fā)者推薦的一種寫法,這種靜態(tài)內(nèi)部類方式在SingletonInnerType 類被裝載時并不會立即實例化,而是在需要實例化時,調用getInstance方法,才會裝載SingletonHolder 類,從而完成對象的實例化。同時,因為類的靜態(tài)屬性只會在第一次加載類的時候初始化,也就保證了SingletonHolder 中的對象只會被實例化一次,并且這個過程也是線程安全的。
靜態(tài)內(nèi)部類也有著一個致命的缺點,就是傳參的問題,由于是靜態(tài)內(nèi)部類的形式去創(chuàng)建單例的,故外部無法傳遞參數(shù)進去,例如Context這種參數(shù),所以,我們創(chuàng)建單例時,可以在靜態(tài)內(nèi)部類與DCL模式里自己斟酌。
5.枚舉方式
package com.d4c.example;
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
//調用
public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
}
這種寫法在《Effective JAVA》中大為推崇,它可以解決兩個問題:
1)線程安全問題。因為Java虛擬機在加載枚舉類的時候會使用ClassLoader的方法,這個方法使用了同步代碼塊來保證線程安全。
2)避免反序列化破壞對象,因為枚舉的反序列化并不通過反射實現(xiàn)。
好了,單例模式的幾種寫法就介紹到這了,最后簡單總結一下單例模式的優(yōu)缺點缺點:不能延時加載。
總結:
匿名內(nèi)部類的方式和單元素的枚舉類型能夠防住反射或反序列化的攻擊,其他幾種則不行。所以推薦這兩種方式創(chuàng)建單例。
引用文章
設計模式:單例模式 (關于餓漢式和懶漢式)
【一起學系列】之單例模式:只推薦三種~
Java 利用枚舉實現(xiàn)單例模式
Java單例模式:為什么我強烈推薦你用枚舉來實現(xiàn)單例模式
Java單例---反射攻擊破壞單例和解決方法