一、單例模式的應用場景
單例模式(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中使用的就是容器式單例模式。