標籤:
單例模式
最簡單但是也挺困難的。
要保證在一個JVM中只能存在一個執行個體,要考慮到如下的情況:
- Java能夠使用那些方式構建對象
- Java在建立對象時多線程並發情況下是否仍然只能建立一個執行個體
Java建立對象的方法:
- new 最常用的,直接使用構造器建立。 每new一次都會產生新的執行個體。所以單例中應該只new一次,當再想用對象時都返回該對象的值
- Class.newInstance() 該方法會調用public 的無參構造器。
為了防止這個方式建立,只要把構造器設定為private的就可以了。這是如果再用這個方法建立會報錯.同時私人構造器也可以解決四處new的問題。
- 反射
Constructor ctt = c.getDeclaredConstructor();
ctt.setAccessible(true);
T t1 = ctt.newInstance();
這樣私人構造器也不行了。解決的辦法是使用抽象類別,這樣就會拋出異常了,不能建立了。或者在構造器中加入判斷如果是第二次構建就拋出異常。
- clone
這個主要由clone()方法的具體行為決定的。如果沒有實現Cloneable介面是不用管這個問題的。
- 還原序列化
還原序列化的時候也會打破單例,解決的方式是寫一個readResolve。這個方法的規則是在還原序列化的時候勇氣傳回值來代替還原序列化的傳回值
還有一個更簡單的辦法是不要實現Serializable介面,這樣序列化的時候就會報錯了
先寫個驗證工具,來驗證這個類是否是單例的
public class SingletonTester { public static <T> void checkClassNewInstance(Class<T> c){ try { T t1 = c.newInstance(); T t2 = c.newInstance(); if(t1 != t2){ System.out.println("Class.newInstance校正失敗,可以建立兩個執行個體"); }else{ System.out.println("Class.newInstance校正通過"); } } catch (Exception e) { System.out.println("不能用Class.newInstance建立,因此Class.newInstance校正通過"); } } public static <T> void checkContructorInstance(Class<T> c){ try { Constructor<T> ctt = c.getDeclaredConstructor(); ctt.setAccessible(true); T t1 = ctt.newInstance(); T t2 = ctt.newInstance(); if(t1 != t2){ System.out.println("ContructorInstance校正失敗,可以建立兩個執行個體"); }else{ System.out.println("ContructorInstance校正通過"); } } catch (Exception e) { System.out.println("不能用反射方式建立,因此ContructorInstance校正通過"); } } public static <T> void testSerializable(T t1){ File objectF = new File("/object"); ObjectOutputStream out = null; try { out = new ObjectOutputStream(new FileOutputStream(objectF)); out.writeObject(t1); out.flush(); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream(objectF)); T t2 = (T) in.readObject(); in.close(); if(t1 != t2){ System.out.println("Serializable校正失敗,可以建立兩個執行個體"); }else{ System.out.println("Serializable校正通過"); } } catch (Exception e) { System.out.println("不能用還原序列化方式建立,因此Serializable校正通過"); } } public static void main(String[] args) { checkClassNewInstance(Singleton3.class); checkContructorInstance(Singleton3.class); testSerializable(Singleton3.getInstance()); }}
這個工具驗證了Class.newInstance攻擊,反射攻擊,還原序列化攻擊,能夠屏蔽著三種攻擊的才是好的單例。
單例1
public class Singleton1{ private Singleton1() { } private static Singleton1 instance; public static Singleton1 getInstance(){ if(instance == null){ instance = new Singleton1(); } return instance; }}
最普通懶漢模式的單例, 私人構造器,靜態方法擷取執行個體,擷取的時候先判空。
測試結果:
不能用Class.newInstance建立,因此Class.newInstance校正通過ContructorInstance校正失敗,可以建立兩個執行個體不能用還原序列化方式建立,因此Serializable校正通過
這個類因為不能被序列化,因此不會受到還原序列化攻擊
因為私人構造器避免了Class.newInstance
但是會被反射攻擊
另外其不是安全執行緒的
單例2
public class Singleton2 { private static Singleton2 sington = new Singleton2(); private Singleton2(){}; public static Singleton2 getInstance(){ return sington; }}
來個典型的餓漢模式的
測試結果:
不能用Class.newInstance建立,因此Class.newInstance校正通過ContructorInstance校正失敗,可以建立兩個執行個體不能用還原序列化方式建立,因此Serializable校正通過
同樣不會有還原序列化及Class.newInstance的問題。
並且沒有並發的問題。
不過其會在不同的時候也初始化一個執行個體出來。個人感覺實際上影響不大
單例3
上面的都會有反射攻擊的問題。來解決它。
public class Singleton3 { private static Singleton3 sington = new Singleton3(); private static int COUNT = 0; private Singleton3(){ if(++COUNT > 1){ throw new RuntimeException("can not be construt more than once"); } }; public static Singleton3 getInstance(){ return sington; }}
測試結果:
不能用Class.newInstance建立,因此Class.newInstance校正通過不能用反射方式建立,因此ContructorInstance校正通過不能用還原序列化方式建立,因此Serializable校正通過
通過加入計數器來解決,這樣雖然解決了反射攻擊,但是卻不是安全執行緒的,另外引入了新的變數也不優雅。下面換個方式:
單例4
public abstract class Singleton4 { private static class SingletonHolder{ private static final Singleton4 INSTANCE = new Singleton4() { }; } private Singleton4(){}; public static Singleton4 getInstance(){ return SingletonHolder.INSTANCE; }}
這個推薦使用
- 用抽象類別解決了反射攻擊
- 用類載入的執行緒安全性解決了並發
- 用內部類實現了lazyloader的目的
- 沒有實現clone
- 沒有實現Serializable介面不會有還原序列化的問題
單例5
下面說下不用內部類的懶漢模式
public class Singleton5 { private static Singleton5 sington = null; private Singleton5(){}; public static Singleton5 getInstance(){ if(sington == null){ // 1 synchronized (Singleton5.class) { if(sington == null){ // 2 sington = new Singleton5(); } } } return sington; }}
如果沒有 //1 的檢查,那麼所有的getInstance()都會進入鎖爭奪,會影響效能,因此加入了檢查。
此外其會被反射攻擊
單例6
上面的會有安全執行緒問題,是由於JVM的重排序機制引起的:
重排序:
JVM在編譯的時候會保證單線程模式下的結果是正確的,但是其中代碼的順序可能會進行重排序,或者亂序,主要是為了更好的利用多cpu資源(亂序), 以及更好的利用寄存器,。
比如1 a = 1; b = 2; a=3;三個語句,如果b執行的時候可能會佔用a的寄存器位置,JVM可能會把a=3語句提到b=2前面,減少寄存器置換次數。
比如上面的 instance = new Singleton5()這部分代碼的偽位元組碼為:
1. memory = allocate() // 分配記憶體
2. init(memory) // 初始化對象
3. instance = memory // 執行個體指向剛才初始化的記憶體位址。
4. 第一次訪問instance
在JVM的時候有可能2.3的位置進行了重新排序,因為JVM只保證構造器執行完之後的結果是正確的,但是執行順序可能會有變化。 這個時候並發調用getInstance的時候就有可能出現如下的情況:
| 時間 |
線程A |
線程B |
| t1 |
A1:指派至的記憶體空間 |
|
| t2 |
A3:設定instance指向記憶體空間 |
|
| t3 |
|
B://1 處判斷instance是否為空白 |
| t4 |
|
B:由於instance不為null,線程B將返回instance引用的對象 |
| t5 |
|
B:instance沒有經過初始化,可能會有未知問題 |
| t6 |
A2:初始化對象 |
|
| t7 |
A:這是對象才是被初始化的 |
|
為瞭解決這個問題,我們可以從兩個方向考慮:制止重排序,或者使重排序對其他線程不可見。
制止重排序的方式單例:
使用JDK1.5之後提供的volatile關鍵字。這個關鍵字的意義在於保證變數的可見度。保證變數的改變肯定會回寫主記憶體,並且關閉java -server模式下的一些最佳化,比如重排序:
public abstract class Singleton6 { private static volatile Singleton6 sington = null; private Singleton6(){}; public static Singleton6 getInstance(){ if(sington == null){ // 1 synchronized (Singleton6.class) { if(sington == null){ // 2 sington = new Singleton6(){};; } } } return sington; }}
還可以,但是代碼有些長,不如Singleton4
單例7
使重排序對其他線程不可見的單例:
public abstract class Singleton7 { private static Singleton7 sington = null; private Singleton7(){}; public static Singleton7 getInstance(){ if(sington == null){ // 1 synchronized (Singleton7.class) { if(sington == null){ // 2 Singleton7 temp = new Singleton7(){}; sington = temp; } } } return sington; }}
另外單例4頁是這樣的,重排序對其他的線程是不可見的
單例8
如果有必要序列化,那麼就需要實現Serializable介面,下面說下這種情況如何解決還原序列化攻擊的問題
public abstract class Singleton8 implements Serializable{ private static class SingletonHolder{ private static final Singleton8 INSTANCE = new Singleton8() { }; } private Singleton8(){}; public static Singleton8 getInstance(){ return SingletonHolder.INSTANCE; } public Object readResolve() { return SingletonHolder.INSTANCE; }}
測試結果:
不能用Class.newInstance建立,因此Class.newInstance校正通過不能用反射方式建立,因此ContructorInstance校正通過Serializable校正通過
這個主要在於方法readResolve, 其返回結果會用來代替還原序列化的結果
單例9
枚舉單例,effectiveJava中推薦的
最後一個了。就是使用枚舉單例了。可以看一下,是極好用的
public enum SingleEnum { INSTANCE; }
測試結果:
不能用Class.newInstance建立,因此Class.newInstance校正通過不能用反射方式建立,因此ContructorInstance校正通過Serializable校正通過
它也成功的避免了各種可能存在的問題:
- 用抽象類別解決了反射攻擊
- 用類載入的執行緒安全性解決了並發
其類載入部分的代碼:
public abstract class Enum{ private Enum{} private static Enum INSTANCE = null; static{ INSTANCE = new Enum(){}; }}
- 用靜態方法初始化保證了安全執行緒,會在類載入的時候初始化
- 沒有實現clone
- 不會有還原序列化的問題, 這個使用javap 仍然沒有看到類似於readObject的原始碼,應該是jdk內部產生位元組碼的時候做了某些操作。
好了,綜上,盡量用枚舉單例,或者是Holder單例吧
可能是最全的Java單例模式討論