一說到單例模式,我想你們首先想到的是懶漢式、惡漢式吧!至於登記式(淘汰的模式,可忽略)。
單例模式有以下特點:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
一、懶漢式單例
先寫一個懶漢式的單例模式。
public class Singleton { private Singleton() {} private static Singleton single=null; public static Singleton getInstance() { if (single == null) { single = new Singleton(); } return single; } }
Singleton通過將構造方法限定為private避免了其他類通過訪問構造器進行實例化,在同一個虛擬機范圍內,Singleton的唯一實例只能通過靜態的getInstance()方法進行訪問。
但是上面的代碼是不考慮線程安全的情況下,也就是說,該實例是存在線程安全的。在並發的情況下是可能出現這種情況,就是a線程先進入getInstance()方法在創建實例化的時候,也就是還沒創建成功,b線程也進入了getInstance()方法,這個時候a線程實例還沒建成功,b線程判斷single為空也開始創建實例,導致會出現創建出兩個實例來。
解決方式有三種:
public static synchronized Singleton getInstance() { if (single == null) { single = new Singleton(); } return single; }
加上synchronized關鍵字,並發的時候也只能一個一個排隊進行getInstance()方法訪問。
public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }
雙重檢查鎖定,這種方式會優於上面一種方式,在並發量高的情況下,不需要排隊進getInstance()方法合理利用系統資源,性能上會優於上面一種。
public class Singleton { private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return LazyHolder.INSTANCE; } }
靜態內部類實現單例模式,這種方式優於上面兩種方式,他即實現了線程安全,又省去了null的判斷,性能優於上面兩種。
二、餓漢式單例
public class Singleton { private Singleton() {} private static final Singleton single = new Singleton(); public static Singleton getInstance() { return single; } }
餓漢式是靜態加載的時候實,不需要擔心線程安全問題。
三、枚舉單例模式
以上兩種方式是在不考慮放射機制和序列化機制的情況下實現的單例模式,但是如果考慮了放射,則上面的單例就無法做到單例類只能有一個實例這種說法了。事實上,通過Java反射機制是能夠實例化構造方法為private的類的。這也就是我們現在需要引入的枚舉單例模式。
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } }
舉個例子就能知道上面的單例不是很安全,以雙重檢索的單例模式為例子,我利用放射,能夠創建出新的實例:
public static void main(String[] args) throws Exception { Singleton s=Singleton.getInstance(); Singleton sual=Singleton.getInstance(); Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton s2=constructor.newInstance(); System.out.println(s+"\n"+sual+"\n"+s2); System.out.println("正常情況下,實例化兩個實例是否相同:"+(s==sual)); System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(s==s2)); }
結果為:
cn.singleton.Singleton@1641e19d cn.singleton.Singleton@1641e19d cn.singleton.Singleton@677323b6 正常情況下,實例化兩個實例是否相同:true 通過反射攻擊單例模式情況下,實例化兩個實例是否相同:false
由此可見雙重檢索模式不是最安全的,無法避免反射的攻擊。
我們檢測一下枚舉的單例模式
public static void main(String[] args) throws Exception{ EnumSingleton singleton1=EnumSingleton.INSTANCE; EnumSingleton singleton2=EnumSingleton.INSTANCE; System.out.println("正常情況下,實例化兩個實例是否相同:"+(singleton1==singleton2)); Constructor<EnumSingleton> constructor= null; constructor = EnumSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); EnumSingleton singleton3= null; singleton3 = constructor.newInstance(); System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3); System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(singleton1==singleton3)); }
結果會報Exception in thread "main" java.lang.NoSuchMethodException。出現這個異常的原因是因為EnumSingleton.class.getDeclaredConstructors()獲取所有構造器,會發現並沒有我們所設置的無參構造器,只有一個參數為(String.class,int.class)構造器,而且在反射在通過newInstance創建對象時,會檢查該類是否ENUM修飾,如果是則拋出異常,反射失敗。所以枚舉是不怕發射攻擊的。
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; }