設計模式之單例模式(Singleton Pattern)深入淺出
單例模式介紹:單例模式是指確保一個類在任何情況下都絕對只有一個例項,並且提供一個全域性的訪問點。隱藏其所有構造方法,屬於創新型模式。
常見的單例有:ServletContext、ServletConfig、ApplicationContext、DBPool
單例模式的優點:
- 在記憶體中只有一個例項,減少記憶體開銷。
- 可以避免對資源的佔用
- 設定全域性訪問點,嚴格控制訪問
單例模式的缺點:
- 沒有介面,擴充套件困難
- 如果要擴充套件單例物件,只有修改程式碼,沒有其他捷徑
以下是單例模式的種類及優缺點分析
餓漢式單例
在單例類首次載入時就建立例項
第一種寫法:
public class HungrySingleton { private static final HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return hungrySingleton; } }
第二種寫法:
public class HungryStaticSingleton { private static final HungryStaticSingleton hungrySingleton; static { hungrySingleton = new HungryStaticSingleton(); } private HungryStaticSingleton() { } public static HungryStaticSingleton getInstance() { return hungrySingleton; } }
缺點:單例例項在類裝載時就構建,浪費資源空間
懶漢式單例
一、懶漢式第一種:
首先先簡單實現以下
public class LazySimpleSingleton { private static LazySimpleSingleton lazySingleton = null; private LazySimpleSingleton() { } public static LazySimpleSingleton getInstance() { if (lazySingleton == null) { lazySingleton = new LazySimpleSingleton(); } return lazySingleton; } }
我們用執行緒測一下在多執行緒場景下會不會出現問題
先建立一個執行緒類
public class ExectorTread implements Runnable { @Override public void run() { LazySimpleSingleton instance = LazySimpleSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + instance); } }
測試
public class LazySimpleSingletonTest { public static void main(String[] args) { Thread t1 = new Thread(new ExectorTread()); Thread t2 = new Thread(new ExectorTread()); t1.start(); t2.start(); System.out.println("Exector End"); } }
執行結果
結果發現建立的物件不一樣
如果在方法上加入鎖會解決問題
public class LazySimpleSingleton { private static LazySimpleSingleton lazySingleton = null; private LazySimpleSingleton() { } public synchronized static LazySimpleSingleton getInstance() { if (lazySingleton == null) { lazySingleton = new LazySimpleSingleton(); } return lazySingleton; } }
雖然jdk1.6之後對synchronized效能優化了不少,但是還是存在一定的效能問題,這種寫法會造成整個類被鎖住,大大降低了效能
於是我們有了新的寫法
二、懶漢式第二種:
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazySingleton = null; private LazyDoubleCheckSingleton() { } // 適中方案 // 雙重檢查鎖 public static LazyDoubleCheckSingleton getInstance() { if (lazySingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazySingleton == null) { lazySingleton = new LazyDoubleCheckSingleton(); } } } return lazySingleton; } }
知識補充:
1、執行緒安全性開發遵循三個原則:
- 原子性:即一個操作或者多個操作要麼全部執行,要麼都不執行
- 可見性:多個執行緒訪問同一變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值
- 有序性:程式執行的順序按照程式碼的先後順序執行
通過對這段程式碼執行緒的debug發現,這裡雙重檢查體現了可見性
2、JVM:CPU在執行的時候會轉換成JVM指令
lazySingleton = new LazySimpleSingleton(); 這行程式碼實際進行了如下操作
- 第一步、分配記憶體給物件
- 第二步、初始化物件
- 第三步、將初始化物件和記憶體地址關聯(賦值)
- 第四步、使用者初次訪問
在多執行緒環境中,第二步和第三步可能會發生顛倒,這就需要指令重排序,於是我們在變數宣告上加上volatile關鍵字就很好的解決了問題
三、懶漢式第三種
通過內部類的方式實現
public class LazyInnerClassSingleton { private LazyInnerClassSingleton() { } public static final LazyInnerClassSingleton getInstance() { return LazyHolder.LAZY; } private static class LazyHolder { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
效能上最優的一種寫法,全程沒有用到synchronized,
通過懶載入餓漢式寫法達到了懶漢式的目的,LazyHolder裡面的邏輯要等到外面呼叫才執行,巧妙地運用了內部類的特性
有人會問這個不用考慮執行緒安全嗎?其實這是利用了JVM底層的執行邏輯,完美的避開了執行緒安全性的問題
但是我們會考慮另一個問題,該類構造器雖然私有了,但是還是會被反射攻擊,難逃反射法眼
我們來測試一下
public class LazyInnerClassSingletonTest { public static void main(String[] args) { try { // 呼叫者裝B,不走尋常路,顯然搞壞了單例 Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class; Constructor<LazyInnerClassSingleton> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true);//強吻(問) LazyInnerClassSingleton instance = constructor.newInstance(); System.out.println(instance); // 正常呼叫 LazyInnerClassSingleton instance2 = LazyInnerClassSingleton.getInstance(); System.out.println(instance2); System.out.println(instance == instance2); } catch (Exception e) { e.printStackTrace(); } } }
執行結果
針對反射問題我們有了以下解決辦法
public class LazyInnerClassSingleton { private LazyInnerClassSingleton() { if (LazyHolder.LAZY != null){ throw new RuntimeException("不允許構建多個例項"); } } public static final LazyInnerClassSingleton getInstance() { return LazyHolder.LAZY; } private static class LazyHolder { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
在私有構造方法上加上判斷,如果已經物件被初始化就丟擲異常
好的,被反射破壞的問題解決了,還會想到另一個問題,如果被反序列化物件還是單例嗎?
知識點補充:反序列化是將已經持久的的位元組碼內容,轉換為IO流,在轉換過程中重新建立物件new。
我們拿餓漢式單例測試一下
單例類:
public class SeriableSingleton implements Serializable { private static final SeriableSingleton singleton = new SeriableSingleton(); private SeriableSingleton() { } public static SeriableSingleton getInstance() { return singleton; } }
測試類:
public class SeriableSingletonTest { public static void main(String[] args) { FileOutputStream fso = null; SeriableSingleton s1 = null; SeriableSingleton s2 = SeriableSingleton.getInstance(); try { fso = new FileOutputStream("SeriableSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fso); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SeriableSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (SeriableSingleton) ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2); } catch (Exception e) { e.printStackTrace(); } } }
執行結果
顯然反序列化破壞了單例
現在我們通過原始碼尋找答案
進入readObject()方法
方法通過呼叫readObject0(false)返回結果,再次進入readObject0()方法
找到object型別,呼叫了checkResolve(readOrdinaryObject(unshared)),進入readOrdinaryObject方法
在這裡我們找到了例項化物件的語句
obj = desc.isInstantiable() ? desc.newInstance() : null;
意思是如果這個物件能被初始化就例項化物件,否則等於null
在這裡打個斷點除錯一下,確實例項化了物件,存在私有構造方法也會例項化物件
接著往下看
如果desc.hasReadResolveMethod()返回true,就呼叫Object rep = desc.invokeReadResolve(obj);返回obj
進入hasReadResolveMethod
原始碼分析過後發現這個hasReadResolveMethod()方法是用來判斷readResolve方法是否存在,如果存在返回true,不存在返回false
再看invokeReadResolve()方法
返回了readResolve這個方法的返回值,
所以經過這個判斷會重新載入物件並返回
接下來我們得出結論,程式碼增加重寫方法readResolve
public class SeriableSingleton implements Serializable { private static final SeriableSingleton singleton = new SeriableSingleton(); private SeriableSingleton() { } public static SeriableSingleton getInstance() { return singleton; } protected Object readResolve() { return singleton; } }
再次執行解決了序列化的問題
但是值得我們注意的是,重寫readResolve方法只不過是覆蓋了反序列化出來的物件,物件還是建立了2次,
由於發生再JVM層面,相對來說比較安全,在之前沒有被引用的物件會被GC回收(JVM知識點)
註冊式單例
一、第一種寫法
使用列舉類實現單例模式,也是《Effictive Java》這本書推薦的寫法
public enum EnumSingleton { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; } }
1、首先判斷執行緒安全性
通過反編譯工具JAD得到列舉類的原始碼,附:JAD下載地址
public final class EnumSingleton extends Enum { public static EnumSingleton[] values() { return (EnumSingleton[])$VALUES.clone(); } public static EnumSingleton valueOf(String name) { return (EnumSingleton)Enum.valueOf(com/zc/singleton/register/EnumSingleton, name); } private EnumSingleton(String s, int i) { super(s, i); } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; } public static final EnumSingleton INSTANCE; private Object data; private static final EnumSingleton $VALUES[]; static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); } }
通過程式碼發現,靜態程式碼塊例項化了單例類,屬於餓漢式單列,不存線上程安全性問題,這個例項化過程發生在JVM層面,所以可以認為懶載入
2、測試序列化
public class EnumSingletonTest {
public static void main(String[] args) { FileOutputStream fso = null; EnumSingleton s1 = null; EnumSingleton s2 = EnumSingleton.getInstance(); s2.setData(new Object()); try { fso = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fso); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(s1.getData()); System.out.println(s2.getData()); System.out.println(s1.getData() == s2.getData()); } catch (Exception e) { e.printStackTrace(); } } }
執行結果 :
列舉類是怎樣避免不被序列化破壞的呢?我們來檢視原始碼
首先進入列舉型別case
進入readEnum方法
通過列舉類物件根據註冊的類名獲取例項然後返回,所以不會建立新的物件
3、測試反射
// 反射 try { Class<EnumSingleton> clazz = EnumSingleton.class; Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); constructor.newInstance(); }catch (Exception e){ e.printStackTrace(); }
執行結果 :
報錯:沒有找到這樣的構造方法
反編譯獲取的類有這樣的一個構造方法
private EnumSingleton(String s, int i) { super(s, i); }
我們用這個構造再例項化一次看看
測試:
// 反射 try { Class<EnumSingleton> clazz = EnumSingleton.class; Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); EnumSingleton instance = constructor.newInstance("zhou", 666); System.out.println(instance); }catch (Exception e){ e.printStackTrace(); }
執行結果:
報錯:不能通過反射建立這個列舉物件
檢視原始碼:
得知如果該類的型別為列舉類,就丟擲異常
總結:從JDK層面就為列舉類不被例項化和反射保駕護航
二、第二種寫法
容器式單例,Spring容器中單例的寫法
public class ContainerSingleton { private ContainerSingleton() {} private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>(); public static Object getBean(String className){ synchronized (ioc){ if (!ioc.containsKey(className)){ Object obj = null; try { obj = Class.forName(className).newInstance(); ioc.put(className,obj); }catch (Exception e){ e.printStackTrace(); } return obj; } return ioc.get(className); } } }
優點:物件方便管理,其實也是屬於懶載入
ThreadLocal
使用ThreadLocal實現單例模式
//偽執行緒安全 public class ThreadLocalSingleton { private ThreadLocalSingleton(){} private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = new ThreadLocal<ThreadLocalSingleton>(){ @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; public static ThreadLocalSingleton getInstance(){ return threadLocalSingleton.get(); } }
測試多執行緒
public class ExectorTread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance()); } }
public class ThreadLocalSingletonTest { public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance()); Thread t1 = new Thread(new ExectorTread()); t1.start(); Thread t2 = new Thread(new ExectorTread()); t2.start(); } }
列印結果
結論:該單例在單個執行緒中可以保持單例,但是每個其他執行緒互相都不一樣
原理:每次獲取例項會從ThreadLocalMap中取值,而每個單例的key就是執行緒名
屬於註冊式單例(容器形式)
應用場景:Spring的orm框架中
以上對單例模式的介紹到此結束,歡迎批評指正。附:原始碼地址