定義:保證一個類僅有一個實例,並提供一個全局訪問點
類型:建立型java
想確保任何狀況下都絕對只有一個實例spring
在內存裏只有一個實例,減小了內存開銷
能夠避免對資源的多重佔用
設置全局訪問點,嚴格控制訪問緩存
沒有接口,擴展困難安全
私有構造器:禁止從單例類外部構造對象
線程安全
延遲加載:使用時才建立
序列化和反序列化安全:序列化和反序列化會對單例模式進行破壞
反射:防護反射攻擊mybatis
注重延遲加載:多線程
public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton(){ } public static LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
可是存在線程安全問題,因此能夠增長synchronized:ide
public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton(){ } public synchronized static LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
可是 synchronized 對性能存在影響,因此可使用Double Check雙重檢查:性能
public class LazyDoubleCheckSingleton { private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; private LazyDoubleCheckSingleton(){ } public static LazyDoubleCheckSingleton getInstance(){ if(lazyDoubleCheckSingleton == null){ synchronized (LazyDoubleCheckSingleton.class){ if(lazyDoubleCheckSingleton == null){ lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); } } } return lazyDoubleCheckSingleton; } }
其中測試
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
這一句代碼包含三個步驟:
1.分配內存給這個對象
2.初始化對象
3.設置lazyDoubleCheckSingleton 指向剛分配的內存地址
在java語言規範中 容許在單線程內,不會改變單線程執行結果的重排序。
因此 2和3步可能會存在指令重排序,在單線程中,不會影響執行結果:
this
此時在多線程中:
此時線程1訪問對象,可是對象在線程0中尚未初始化完成,可能就會報異常。
解決方案:
方案一、不容許二、3步驟重排序:
使用volatile關鍵字:
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; private LazyDoubleCheckSingleton(){ } public static LazyDoubleCheckSingleton getInstance(){ if(lazyDoubleCheckSingleton == null){ synchronized (LazyDoubleCheckSingleton.class){ if(lazyDoubleCheckSingleton == null){ lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); } } } return lazyDoubleCheckSingleton; } }
使用了volatile後,全部線程均可以看到共享內存的最新狀態,保證了內存的可見性。用volatile關鍵字修飾的變量,在進行寫操做時,會多出一些彙編代碼,將當前處理器緩存行的數據寫回到內存,其中涉及到緩存一致性協議。
方案二、容許重排序,但不容許其餘線程看到這個重排序,即靜態內部類
基於類初始化的延遲加載解決方案
public class StaticInnerClassSingleton { private static class InnerClass{ private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton getInstance(){ return InnerClass.staticInnerClassSingleton; } private StaticInnerClassSingleton(){ } }
原理:存在Class對象的初始化鎖,而且非構造線程,是看不到指令重排序的。
線程0初始化Class,線程1看不到初始化過程。因此靜態內部類這種方法的核心在於InnerClass這個類的對象初始化鎖
補充:類在如下幾種狀況下被初始化,1.實例被建立(new、反射、序列化),2.靜態方法被調用,3.靜態成員被賦值,4.很是量靜態成員被使用,5.頂級類中有嵌套的斷言語句,6.子類被初始化
最簡單的寫法:
public class HungrySingleton { private final static HungrySingleton hungrySingleton; static{ hungrySingleton = new HungrySingleton(); } private HungrySingleton(){ } public static HungrySingleton getInstance(){ return hungrySingleton; } }
優勢是類加載的時候就完成了初始化,避免了線程同步的問題
缺點是沒有延遲加載的效果,可能形成累成內存浪費
餓漢與懶漢之間最大的區別就是延遲加載:餓漢式很餓,一上來就想吃東西,立刻就把對象建立好了;而懶漢式很是懶,不用它的時候都不會建立這個對象。
如下序列化和反序列化 將會破壞單例模式:
// 實現序列化接口 public class HungrySingleton implements Serializable { private final static HungrySingleton hungrySingleton; static{ hungrySingleton = new HungrySingleton(); } private HungrySingleton(){ } public static HungrySingleton getInstance(){ return hungrySingleton; } }
測試類:
public class Test { public static void main(String[] args) throws IOException, ClassNotFoundException { HungrySingleton instance = HungrySingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(instance); File file = new File("singleton_file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); HungrySingleton newInstance = (HungrySingleton) ois.readObject(); // 將會輸出兩個不一樣的內存地址 System.out.println(instance); System.out.println(newInstance); } }
解決方法:反序列化是經過反射生成對象,在這個過程當中,會判斷是否存在並調用readResolve方法
因此可經過增長readResolve方法防止反序列化:
public class HungrySingleton implements Serializable{ private final static HungrySingleton hungrySingleton; static{ hungrySingleton = new HungrySingleton(); } private HungrySingleton(){ } public static HungrySingleton getInstance(){ return hungrySingleton; } private Object reaResolve(){ // 返回單例對象 return hungrySingleton; } }
可是在這個過程當中,仍然被建立了新的對象,只是最後沒有返回而已。
public class Test { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class; Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); HungrySingleton instance = HungrySingleton.getInstance(); HungrySingleton newInstance = declaredConstructor.newInstance(); System.out.println(instance); System.out.println(newInstance); // 輸出false System.out.println(instance == newInstance); } }
對於餓漢式單例、靜態內部類單例,由於是在類初始化時就建立了對象,因此可在構造器中進行反射防護:
public class HungrySingleton implements Serializable{ private final static HungrySingleton hungrySingleton; static{ hungrySingleton = new HungrySingleton(); } private HungrySingleton(){ // 反射防護,當類在初始化時,單例就會被初始化,爲第一次調用;反射時,爲第二次調用就會報錯 if(hungrySingleton != null){ throw new RuntimeException("單例構造器禁止反射調用"); } } public static HungrySingleton getInstance(){ return hungrySingleton; } private Object readResolve(){ // 返回單例對象 return hungrySingleton; } }
而對於不是在類初始化時建立對象的單例模式,則沒法防護反射攻擊,例如懶漢式單例模式:
public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton(){ if(lazySingleton != null){ throw new RuntimeException("單例構造器禁止反射調用"); } } public synchronized static LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
由於在被反射攻擊的時候,單例可能尚未被建立,因此會產生不一樣實例,測試類:
public class Test { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { // 反射攻擊 Class<LazySingleton> lazySingletonClass = LazySingleton.class; Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); // 先反射 LazySingleton newInstance = declaredConstructor.newInstance(); // 後取單例,由於類中的實例仍爲null,因此構造器的判斷沒有起到想要的做用 LazySingleton instance = LazySingleton.getInstance(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); } }
能夠增長信號量進行控制:
public class LazySingleton { private static LazySingleton lazySingleton = null; private static boolean flag = true; private LazySingleton(){ if (flag){ flag = false; } else { throw new RuntimeException("單例構造器禁止反射調用"); } } public synchronized static LazySingleton getInstance(){ if(lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; } }
可是信號量仍然能夠被修改,以達到反射攻擊:
public class Test { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, NoSuchFieldException, InvocationTargetException { Class objectClass = LazySingleton.class; Constructor c = objectClass.getDeclaredConstructor(); c.setAccessible(true); LazySingleton o1 = LazySingleton.getInstance(); Field flag = o1.getClass().getDeclaredField("flag"); flag.setAccessible(true); // 修改信號量 flag.set(o1,true); LazySingleton o2 = (LazySingleton) c.newInstance(); System.out.println(o1); System.out.println(o2); // 返回false System.out.println(o1==o2); } }
枚舉類型自然的可序列化機制,可以強有力得保證不會屢次實例化的狀況。即便在複雜的序列化或者反射攻擊下,枚舉模式都沒有問題。
public enum EnumInstance { INSTANCE{ protected void printTest(){ System.out.println("Geely Print Test"); } }; protected abstract void printTest(); private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumInstance getInstance(){ return INSTANCE; } }
在ObjectInputStream中,對於枚舉類型,是經過枚舉類直接得到惟一的枚舉常量,沒有建立新的對象,維護了枚舉的單例屬性:
而對於反射,在調用
objectClass.getDeclaredConstructor();
時會直接報錯:
java.lang.NoSuchMethodException
緣由在於Enum自己就只有一個構造器:
而若是調用
Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); EnumInstance instance = (EnumInstance) constructor.newInstance("11",22);
也會直接報錯:java.lang.IllegalArgumentException: Cannot reflectively create enum objects
若是經過jad反編譯枚舉類,能夠看到:1.class類爲final的;2.構造器爲private;3.聲明的枚舉對象是static和final的;4.枚舉對象在static代碼塊中實例化
因此枚舉單例是最安全的單例模式
public class ContainerSingleton { private ContainerSingleton(){ } private static Map<String,Object> singletonMap = new HashMap<String,Object>(); public static void putInstance(String key,Object instance){ if(StringUtils.isNotBlank(key) && instance != null){ if(!singletonMap.containsKey(key)){ singletonMap.put(key,instance); } } } public static Object getInstance(String key){ return singletonMap.get(key); } }
容器單例與享元模式類似
優勢:統一管理,節省資源,至關於緩存
缺點:存在線程安全問題
public class ThreadLocalInstance { private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal = new ThreadLocal<ThreadLocalInstance>(){ @Override protected ThreadLocalInstance initialValue() { return new ThreadLocalInstance(); } }; private ThreadLocalInstance(){ } public static ThreadLocalInstance getInstance(){ return threadLocalInstanceThreadLocal.get(); } }
這個單例 並不能保證整個應用全局惟一,但能保存線程惟一。
ThreadLocal會爲每個線程提供一個變量副本,自己是基於ThreaLocalMap實現的,維持了線程間的隔離。原理是以空間換時間的方式,會建立不少對象,在一個線程裏會建立惟一的一個對象。在多線程訪問的時候,彼此不會相互影響。