單例的7種實現方式和破壞單例的序列化和反射問題及解決方法
七種單例模式
是一種常用的軟件設計模式。在它的核心結構中只包含一個被稱爲單例的特殊類。通過單例模式可以保證系統中,應用該模式的一個類只有一個實例。即一個類只有一個對象實例。
一、 餓漢式(使用全局的靜態常量實現,線程安全)
public class Singleton {
private final static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
這種方式基於classloder機制避免了多線程的同步問題。instance在類加載的時候就實例化了,所以不是懶加載。
二、 餓漢式(使用靜態代碼塊實現,線程安全)
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
這種方式也是基於classloder機制避免了多線程的同步問題。instance在類加載的時候就實例化了,所以不是懶加載。
三、 懶漢式(內部方法實現,非線程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
四、 懶漢式(同步方法實現,線程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
五、懶漢式(同步代碼塊實現,線程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
六、雙重檢查(懶加載+雙非空判斷,線程安全)
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
七 靜態內部類(線程安全)
public class Singleton {
private Singleton (){}
//內部類
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static final Singleton getInstance() {
return SingletonHolder.instance;
}
}
這種方式也是基於classloder機制避免了多線程的同步問題。至於這個單例是否是懶加載。如果我們知道一個類的靜態內部類何時加載的,就會知道。 一個類加載了,它的靜態內部類不會被加載,除非我們調用這個類的變量或者方法,這個類纔會加載。
所以instance實例不會因爲Singleton類的加載而加載,我們只有調用getInstance()方法的時候纔會去加載SingletonHolder類,從而實例化instance
八、 枚舉(線程安全)
public enum Singleton {
//Singleton的單實例
instance;
//測試使用
public void getMess(){
System.out.println("ok");
}
}
這種方式是《Effective Java》作者推薦的方式,他能夠避免多線程同步問題。
枚舉可解決線程安全問題
定義枚舉時使用enum和class一樣,是Java中的一個關鍵字。就像class對應用一個Class類一樣,enum也對應有一個Enum類。
通過將定義好的枚舉反編譯,我們就能發現,其實枚舉在經過javac
的編譯之後,會被轉換成形如public final class T extends Enum
的定義。
而且,枚舉中的各個枚舉項同事通過static
來定義的。如:
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反編譯後代碼爲:
public final class T extends Enum
{
//省略部分內容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
static
類型的屬性會在類被加載之後被初始化,當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的加載和初始化過程都是線程安全的(因爲虛擬機在加載枚舉的類的時候,會使用ClassLoader的loadClass方法,而這個方法使用同步代碼塊保證了線程安全)。所以,創建一個enum類型是線程安全的。
也就是說,我們定義的一個枚舉,在第一次被真正用到的時候,會被虛擬機加載並初始化,而這個初始化過程是線程安全的。而我們知道,解決單例的併發問題,主要解決的就是初始化過程中的線程安全問題。
所以,由於枚舉的以上特性,枚舉實現的單例是天生線程安全的。他是隨着類的加載而加載的,所以它不是懶加載。
破壞單例模式的反序列化和反射
我們創建完成的單例模式,其實並不是那麼完美。 上面列舉的7中單例模式,除了枚舉單例模式
外,其他的6中都有可能出現,使用反序列化和反射,從而破壞單例模式,使我們的單例不在單例了。
反序列化破壞單例問題
以 使用全局的靜態常量實現的餓漢式爲例,看看反序列化破壞單例的情況。
如果測試反序列化問題,那麼我們的類必須實現標記接口Serializable,只有實現Serializable接口的類的實例,才能進行序列化寫入文件,才能造成反序列化問題。
public class Singleton implements Serializable {
private Singleton(){}
private static final Singleton instance= new Singleton();
public static Singleton getInstance(){
return instance;
}
}
進行測試:
@Test
public void testSynchronized() throws IOException, ClassNotFoundException {
Singleton singleton = Singleton.getInstance();
// 將HungerSingletons實例寫入文件
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("Serializable"));
outputStream.writeObject(singleton);
outputStream.flush();
outputStream.close();
//讀取文件中的實例
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("Serializable"));
Singleton serializableSingleton = (Singleton)inputStream.readObject();
inputStream.close();
//判斷singleton和反序列化的實例serializableSingleton的內存地址是否相同
System.out.println("singleton 是否等於 serializableSingleton " + (singleton == serializableSingleton));
//查看兩個實例的內存地址
System.out.println("singleton內存地址 " +singleton);
System.out.println("serializableSingleton內存地址 " +serializableSingleton);
}
測試結果: 我們從結果中可以看出,使用反序列化創建的實例和我們創建的實例,不是同一個實例。破壞的單例模式。
singleton 是否等於 serializableSingleton false
singleton內存地址 pattern.singleton.Singleton@15327b79
serializableSingleton內存地址 pattern.singleton.Singleton@470e2030
防止反序列化破壞單例
我們上面的的反序列化攻擊的例子,使單例模式創建除了兩個實例。出現這樣的原因是因爲我們inputStream.readObject()方法。
任何一個readObject方法,不管是顯式的還是默認的,它都會返回一個新建的實例,這個新建的實例不同於該類初始化時創建的實例。
解決方法: 在我們單例類中加入readResolve()讓它直接返回我們自己創建的實例。
public class Singleton implements Serializable {
private Singleton(){}
private static final Singleton instance= new Singleton();
public static Singleton getInstance(){
return instance;
}
//添加此方法後,解決反序列化的問題
private Object readResolve(){
return instance;
}
}
測試結果: 再次測試,結果顯示反序列化後,我們得到的實例依然是同一個實例,順利的解決了反序列化問題。
singleton 是否等於 serializableSingleton true
singleton內存地址 pattern.singleton.Singleton@15327b79
serializableSingleton內存地址 pattern.singleton.Singleton@15327b79
反射破壞單例問題
@Test
public void testReflect() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//使用單例模式的getInstance()創建對象
Singleton singleton = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("驗證是單例模式: " +(singleton == singleton2));
// 反射創建實例化對象
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
//setAccessible(true)可以獲取私有構造函數。
declaredConstructor.setAccessible(true);
Singleton reflectSingleton = declaredConstructor.newInstance();
//比較getInstance()方法創建的實例和反射創建實例對象是否是一個實例
System.out.println("reflectSingleton是否等於singleton: " + (reflectSingleton == singleton));
}
結果
驗證是否單例模式: true
reflectSingleton是否等於hungerStaitcConstantSingleton: false
reflectSingleton pattern.singleton.Singleton@4ee285c6
singleton pattern.singleton.Singleton@621be5d1
防止反射破壞單例
對私有構造方法的改造,實現防止反射破壞單例
反射是調用我們的私有構造方法,來創建實例的。我們的防護,可以圍繞私有構造方法實現。
我們可以在私有構造方法內進行判斷,如果進行已經有實例了,那麼就拋出異常。
我們使用 餓漢式(使用全局靜態常量實現的單例)來進行測試。
public class Singleton implements Serializable {
/**
* 反射通過私有構造創建實例
* 在私有構造方法中判斷是否已經創建實例,如果創建,拋出異常
*/
private Singleton(){
if (instance != null) {
throw new RuntimeException("單例模式, 不能重複創建實例");
}
}
private static final Singleton instance= new Singleton();
public static Singleton getInstance(){
return instance;
}
}
使用測試類測試
@Test
public void testReflect() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//使用單例模式的getInstance()創建對象
Singleton singleton = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("驗證是否單例模式: " +(singleton == singleton2));
// 反射創建實例化對象
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
//setAccessible(true)可以獲取私有構造函數。
declaredConstructor.setAccessible(true);
Singleton reflectSingleton = declaredConstructor.newInstance();
//比較getInstance()方法創建的實例和反射創建實例對象是否是一個實例
System.out.println("reflectSingleton是否等於singleton: " + (reflectSingleton == singleton));
}
測試結果:
我們可以看見,我們先用正常的情況獲取實例,再用反射創建實例時,拋出了異常。如果我們先用反射創建實例,再用正常情況獲取實例。 依然會報錯。這種情況就解決了反射破解單例的情況。
我們看另一種情況 用懶漢式來測試。
我們測試時,就不先正常獲取實例,再反射獲取實例。 我們直接先反射獲取再正常獲取。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
測試類:
@Test
public void testReflect() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
// 反射創建實例化對象
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
//setAccessible(true)可以獲取私有構造函數。
declaredConstructor.setAccessible(true);
Singleton reflectSingleton = declaredConstructor.newInstance();
//使用單例模式的getInstance()創建對象
Singleton singleton = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("驗證是否單例模式: " +(singleton == singleton2));
//比較getInstance()方法創建的實例和反射創建實例對象是否是一個實例
System.out.println("reflectSingleton是否等於hungerStaitcConstantSingleton: " + (reflectSingleton == singleton));
System.out.println("reflectSingleton " + reflectSingleton);
System.out.println("singleton " + singleton);
}
結果:
驗證是否單例模式: true
reflectSingleton是否等於hungerStaitcConstantSingleton: false
reflectSingleton pattern.singleton.Singleton@4ee285c6
singleton pattern.singleton.Singleton@621be5d1
通過結果我們發現,這次我們又出現了反射破壞單例的情況。
總結:
我們根據這兩種情況可以看出對私有構造方法的改造,實現防止反射破壞單例這種情況並不是通用了。它並不能解決所有單例模式的反射攻擊的情況。
使用範圍: 對私有構造方法的改造,實現防止反射破壞單例這種方式,只能針對全局靜態常量創建實例或者靜態代碼塊創建實例的這種非懶加載情況(第一種單例模式、第二種單例模式和第七中單例模式都適用)。 java基礎的時候,就學過像全局靜態常量和靜態代碼塊,它們是屬於類的,它們會隨着類的加載而加載。所以不管我們使用什麼方法,他都會在類被加載的時候,就創建好實例,所以我們即使先使用反射的方式創建實例,它也會先我們一步將類加載進來,並創建實例,是我們使用反射獲取實例失敗。
雖然第七種靜態內部類的方法是懶加載的,但是實際上,靜態內部類中創建實例依然是隨着類的加載而加載的,所以它適用改造私有構造方法的解決方法。
如果我們使用的單例模式不是懶加載,那麼這個對私有構造方法的改造,實現防止反射破壞單例這種方法就不適用。(單例模式的第三種,第四種,第五種,第六種都不適用)
總結:
從這篇文章中我們也看到了單例模式的七種實現方式和優缺點。以及單例模式中可能存在的問題。
我在下面的表格中列舉了這七種實現方式的優缺點和他們是否存在反序列化和反射攻擊的情況。
懶加載 | 線程安全 | 防止反序列化 | 防止反射 | |
---|---|---|---|---|
餓漢式 (全局靜態常量實現) |
否 | 是 | 否 | 否 |
餓漢式 (靜態代碼塊實現) |
否 | 是 | 否 | 否 |
懶漢式 (內部方法實現) |
是 | 否 | 否 | 否 |
懶漢式 (同步方法實現) |
是 | 是 | 否 | 否 |
懶漢式 (同步代碼塊實現) |
是 | 是 | 否 | 否 |
雙重檢查 | 是 | 是 | 否 | 否 |
靜態內部類 | 是 | 是 | 否 | 否 |
枚舉 | 否 | 是 | 是 | 是 |
Hollis的爲什麼我牆裂建議大家使用枚舉來實現單例。
Wenlong_L的設計模式之單例模式六(防反射攻擊)
這邊文章並非是我原創的,寫這篇文章主要也是想留個底。其中參考了大量其他博主的博文。這些博主寫的都很詳細比我好多了,大家可以看看。
因爲這篇文章,我改了很多次中間可能有不對的地方,如果大家發現,麻煩給我指出,謝謝。