Spring中常見的設計模式——單例模式
一、單例模式的應用場景
單例模式(singleton Pattern)是指確保一個類在任何情況下都絕對只有一個例項,並提供一個全域性訪問點。J2EE中的ServletContext,ServletContextConfig等;Spring中的ApplicationContext、資料庫連線池等。
二、餓漢式單例模式
餓漢式單例模式在類載入的時候就立即初始化,並且建立單例物件。它是絕對的執行緒安全、線上程還沒出現以前就實現了,不可能存在訪問安全問題。
優點:沒有增加任何鎖,執行效率高,使用者體驗比懶漢式好。
缺點:類載入的時候就初始化了,用不用都進行,浪費記憶體。
Spring 中IoC容器ApplocationContext本身就是典型的餓漢式單例模式:
public class HungrySingleton { private static final HungrySingleton h = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return h; } }
餓漢式單例模式適用於單例物件較少的情況。
三、懶漢式單例模式
被外部呼叫才會載入:
public class LazySimpleSingleton { private LazySimpleSingleton() { } private static LazySimpleSingleton lazy = null; public static LazySimpleSingleton getInstance() { if (lazy == null) { lazy = new LazySimpleSingleton(); } return lazy; } }
利用執行緒建立例項:
public class ExectorThread implements Runnable { @Override public void run() { LazySimpleSingleton simpleSingleton = LazySimpleSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + simpleSingleton); } }
客戶端程式碼:
public class LazySimpleSingletonTest { public static void main(String[] args) { Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("END"); } }
結果:
END Thread-1:singleton.Lazy.LazySimpleSingleton@298c37fd Thread-0:singleton.Lazy.LazySimpleSingleton@6ebc1cfd
可以看到 產生的兩個例項的記憶體地址不同說明產生了兩個例項,大家可以通過以下打斷點的方式實現不同Thread執行狀態見進行切換。
要解決執行緒問題第一反應是加 synchronized 加在建立例項的地方:public static synchronized LazySimpleSingleton getInstance(),但當執行緒數量較多時,用Synchronized加鎖,會使大量執行緒阻塞,就需要更好的解決辦法:
public static LazySimpleSingleton getInstance() { if (lazy == null) { synchronized (LazySimpleSingleton.class) { if (lazy == null) { lazy = new LazySimpleSingleton(); } } } return lazy; }
synchronized (lock) lock這個物件就是 “鎖”,當兩個並行的執行緒a,b,當a先進入同步塊,即a先拿到lock物件,這時候a就相當於用一把鎖把synchronized裡面的程式碼鎖住了,現在只有a才能執行這塊程式碼,而b就只能等待a用完了lock物件鎖之後才能進入同步塊。但是用到 synchronized 總歸是要上鎖的,對效能還是有影響,那就用這種方式:用內部類的方式進行懶載入。
public class LazyInnerClassSingleton { private LazyInnerClassSingleton() { } private static final LazyInnerClassSingleton getIngestance() { return LazyHolder.LAZY; } private static class LazyHolder { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
內部類在LazyInnerClassSingleton類載入時載入,解決了餓漢式的效能問題,LazyInnerClassSingleton在內部類載入時,getIngestance()方法被呼叫之前例項化,解決了執行緒不安全問題。
四、反射破壞單例
public class LazyInnerClassSingletonTest { public static void main(String[] args) { try { Class<?> clazz = LazyInnerClassSingleton.class; //通過反射回去私有構造方法 Constructor constructor = clazz.getDeclaredConstructor(null); //強制訪問 constructor.setAccessible(true); //暴力初始化 Object o1 = constructor.newInstance(); //建立兩個例項 Object o2 = constructor.newInstance(); System.out.println("o1:" + o1); System.out.println("o2:" + o2); } catch (Exception e) { e.printStackTrace(); } } }
結果:
o1:singleton.Lazy.LazyInnerClassSingleton@1b6d3586 o2:singleton.Lazy.LazyInnerClassSingleton@4554617c
建立了兩個例項,違反了單例,現在在構造方法中做一些限制,使得多次重複建立時,丟擲異常:
private LazyInnerClassSingleton() { if (LazyHolder.class != null) { throw new RuntimeException("不允許建立多個例項"); } }
這應該就是最好的單例了,哈哈哈。
五、註冊式單例模式
註冊式單例模式又稱為登記式單例模式,就是將每個例項都登記到某個地方,使用唯一標識獲取例項。註冊式單例模式有兩種:列舉式單例模式、容器式單例模式。註冊式單例模式主要解決通過反序列化破壞單例模式的情況。
1.列舉式單例模式
public enum EnumSingleton { INSTANCE; private Object data; public Object getData() { return data; } public void steData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; } }
測試程式碼:
public class EnumSingletonTest { public static void main(String[] args) { try { EnumSingleton instance1 = EnumSingleton.getInstance(); EnumSingleton instance2 = null; instance1.steData(new Object()); FileOutputStream fos = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); instance2 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(instance1.getData()); System.out.println(instance2.getData()); } catch (Exception e) { e.printStackTrace(); } } }
結果:
java.lang.Object@568db2f2 java.lang.Object@568db2f2
那列舉式單例是如何解決反序列化得問題呢?
通過反編譯,可以在EnumSingleton.jad檔案中發現static{} 程式碼塊,列舉式單例模式在靜態程式碼塊中給INSTANCE進行了賦值,是餓漢式單例模式的實現。檢視JDK原始碼可知,列舉型別其實通過類名和類物件找到一個唯一的列舉物件。因此,列舉物件不可能被類載入器載入多次。
當你試圖用反射破壞單例時,會報 Cannot reflectively create enum objects ,即不能用反射來建立列舉型別。進入Customer的newInstance(),其中有判斷:如果修飾符是Modifier.ENUM,則直接丟擲異常。JDK列舉的語法特殊性及反射也為美劇保駕護航,讓列舉式單例模式成為一種比較優雅的實現。
2.容器式單例
public class ContainerSingleton { private ContainerSingleton() { } private static Map<String, Object> ioc = new ConcurrentHashMap<>(); public static Object getBean(String className) { synchronized (ioc) { if (!ioc.containsKey(className)) { Object o = null; try { o = Class.forName(className).newInstance(); ioc.put(className, o); } catch (Exception e) { e.printStackTrace(); } return o; } else { return ioc.get(className); } } } }
spring中使用的就是容器式單例模式。
&n