接下來的系列文章咱們會談設計模式,設計模式不只僅存在Java開發語言中,而是遍佈軟件領域且相當重要,是前輩開發總結的經驗,一種設計思想,一種架構;在軟件開發中,惟一不變的就是需求的變化,開發人員不只要知足當下的功能需求,還要考慮對後續可能的變化,設計的系統就應有良好的拓展性。在公司接手上一任的代碼,繼續開發新功能,若是設計的拓展性很差的話,後期開發會很困難,費時費力,還可能對以前的功能有影響,內心也是忐忑不安,同時也給測試人員添加負擔,改動點增多,測試範圍增大等等,可見設計模式的重要性。java
本文講述較爲簡單的單例模式,單例模式要保證系統中對象惟一,這不是獲取對象方的責任,是對象提供方保證這個對象在系統中就只能存在一個。如何保證對象的惟一性,就要從建立對象的角度,建立對象能夠經過構造方法,Clone對象,反序列化時建立對象,反射四種方式,那麼就須要讓類內部建立惟一對象,不讓外部直接建立,只提供一個方法供外部獲取對象。因此單例模式中第一步構造方法私有,不讓外部new 對象,其次實現單例模式的類不會實現Cloneable接口,則不支持Clone對象;前2種方式都能避免,主要是反序列化和反射機制容易破壞單例。如下咱們來分別討論單例模式的幾種方式和其存在的問題,以及反序列化和反射如何破壞單例,怎樣去避免,如何合理設計單例模式?面試
建立對象四種方式:設計模式
建立單例的常見幾種方式:安全
在須要使用的時候,才建立對象(延遲實例化),存在多線程安全問題。多線程
package designpattern.singleton; /** * @author zdd * 2020/1/10 5:15 下午 * Description: 懶漢式建立單例 */ public class LazyInstantiateTest { private static LazyInstantiateTest INSTANCE; //一、私有構造方法,防止被其餘類建立對象 private LazyInstantiateTest(){}; //二、對外提供靜態公共方法獲取單例對象 public static LazyInstantiateTest getInstance() { if(INSTANCE == null) { INSTANCE = new LazyInstantiateTest(); } return INSTANCE; } }
也稱預加載方式,類在加載初始化時就建立單例對象,餓漢搶食般地建立對象,所以以「餓漢」形容,不存在線程安全問題,可是會佔用內存,類一被加載進來就實例化對象到堆中,可能很長時間才被使用或者未被使用,如此形成資源浪費。架構
package designpattern.singleton; import java.io.Serializable; /** * @author zdd * 2020/1/10 5:31 下午 * Description: 餓漢式實現單例 */ public class HungryTest implements Serializable { private static HungryTest INSTANCE = new HungryTest(); private HungryTest() {}; public static HungryTest getInstance() { return INSTANCE; } }
package designpattern.singleton; /** * @author zdd * 2020/1/10 5:42 下午 * Description: 雙檢鎖單例 */ public class DoubleCheckTest { private static DoubleCheckTest INSTANCE; private DoubleCheckTest() {} public static DoubleCheckTest getInstance() { //1,第一次判空爲了提升程序效率 if(INSTANCE ==null) { //加鎖,這裏使用的監視器對象是該類的字節碼對象 synchronized (DoubleCheckTest.class){ //二、第二次判空是爲了解決多線程安全問題 if (INSTANCE == null) { INSTANCE = new DoubleCheckTest(); } } } return INSTANCE; } }
靜態內部類藉助的是類加載機制,內部類只有在被調用的時候才加載進來,實現延遲建立對象,是餓漢式的改進,既避免了初始化就建立對象佔用內存,又能避免懶漢式的線程安全問題。併發
package designpattern.singleton; import java.io.Serializable; /** * @author zdd * 2020/1/10 5:55 下午 * Description: 靜態內部類單例 */ public class StaticInnerClassTest { //內部類 private static class InstanceInnerClass { private final static StaticInnerClassTest INSTANCE = new StaticInnerClassTest(); } private StaticInnerClassTest(){} public static StaticInnerClassTest getInstance() { return InstanceInnerClass.INSTANCE; } }
網上有個面試題測試
面試官問:如何在不使用關鍵字synchronized、Lock鎖的狀況下,保證線程安全地實現單例模式?atom
可以線程安全建立單例,除了枚舉外,有靜態內部類和雙檢鎖方式,雙檢鎖用了關鍵字synchronized,靜態內部類利用的類加載的機制,底層也是含有加鎖操做的。要想實現不用鎖,能夠參考循環CAS,無阻塞輪詢,利用cas自旋鎖原理。線程
首先寫一個自旋鎖類
package designpattern.singleton; import cas.SpinLockTest; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * @author zdd * 2020/1/10 6:59 * Description: CAS無阻塞自旋鎖 */ public class CasLock { static AtomicReference<Thread> atomicReference = new AtomicReference<>(); public static void lock() { Thread currentThread = Thread.currentThread(); for (;;) { boolean flag =atomicReference.compareAndSet(null,currentThread); if(flag) { break; } } } public static void unLock() { Thread currentThread = Thread.currentThread(); Thread momeryThread = atomicReference.get(); //比較內存中線程對象與當前對象,不相等就拋出異常,防止未獲取到鎖的線程調用 unlock if(currentThread != momeryThread) { throw new IllegalMonitorStateException(); } //釋放鎖 atomicReference.compareAndSet(currentThread,null); } }
實現雙檢鎖變式單例模式
package designpattern.singleton; import cas.SpinLockTest; /** * @author zdd * 2020/1/10 6:46 * Description: cas實現單例,實際是cas自旋鎖,在synchronized阻塞式加鎖的改進,無阻塞式加鎖 */ public class SingletonCasTest { private static SingletonCasTest INSTANCE; private static CasLock spinLock = new CasLock(); private SingletonCasTest() {}; public static SingletonCasTest getInstance() { if(INSTANCE == null) { spinLock.lock(); if (INSTANCE == null) { INSTANCE = new SingletonCasTest(); } spinLock.unLock(); } return new SingletonCasTest(); } }
枚舉類是《Effective Java》書中推薦的實現單例方式,由於其自然的可防止反序列化和反射破解單例的惟一性,保證有且僅有一個對象,
因太簡潔,可讀性不強。
package designpattern.singleton; /** * @author zdd * 2020/1/10 6:43 下午 * Description: */ public enum SingletonEnum{ INSTANCE; }
一是須要考慮線程安全問題,這是懶漢式存在的問題,爲了解決該問題,能夠將getInstance() 方法加上synchronized關鍵字或者在方法內部加同步代碼塊,或者用Lock鎖機制,這樣會致使多線程在獲取單例對象時線程安全了,可是效率會下降,同步代碼塊會比同步方法效率更高一些,主要是同步代碼塊應該儘量的縮小代碼塊的包含範圍(標準是剛好包括臨界區部分),粒度越小,併發度才更高。
二是反序列化問題,在須要將對象序列化與反序列化時,首先讓該單例類實現Serializable接口(標誌接口,無內容,實現類可序列化),然而存在的問題就是在反序列化時會新建立一個對象,這樣就違背了單例模式的對象惟一性。
將對象先轉爲字節寫入到輸入流中(序列化過程),再從輸出流中讀取字節,再轉換爲對象 (反序列化)
代碼示例以下:
package designpattern.singleton; import java.io.*; /** * @author zdd * 2020/1/10 7:23 下午 * Description: 反序列化破壞單例對象惟一性 */ public class DeserializableProblemTest { public static void main(String[] args) throws IOException, ClassNotFoundException { //先將對象加載到輸入流中,在到輸出流獲取對象,以餓漢式單例爲例 HungryTest hungry1 = HungryTest.getInstance();; HungryTest hungry2 = null; //1,將單例對象寫入流中 ByteArrayOutputStream ops = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(ops); oos.writeObject(hungry1); //2,再從流中讀出,轉換爲對象 ByteArrayInputStream ips= new ByteArrayInputStream(ops.toByteArray()); ObjectInputStream ois = new ObjectInputStream(ips); hungry2 =(HungryTest) ois.readObject(); //三、判斷是否爲同一個對象 System.out.println(hungry1 ==hungry2); } }
運行結果: 證實反序列化後又新建立了對象
false
解決反序列化問題:在HungryTest類中添加以下方法
//防止反序列化破壞單例 private Object readResolve() { return INSTANCE; }
再執行運行結果爲 true ,證實是同一個對象,未建立新對象。
爲何添加一個readResolve 方法就能夠防止反序列化建立新的對象呢?
進入ObjectInputStream的 readObject() 可見,下面只列出關鍵代碼位置,詳細可本身查看源碼
首先類要支持序列化,經過反射建立新對象賦值給obj
繼續往下看,這裏有if判斷,知足3個條件,其中hasReadResolveMethod判斷是否有readResolve方法,有則調用該方法,最後obj被readResolve返回對象覆蓋。
那麼readResolveMethod須要知足什麼要求? 知足如下3個條件便可
參考博客:單例模式的攻擊之序列化與反序列化
三是反射,咱們知道Java中反射幾乎是無所不能,你不讓我建立對象,那就暴力反射建立,咱們如何防止反射破解單例?
暴力反射破壞單例示例:
package designpattern.singleton; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * @author zdd * 2020/1/13 2:49 下午 * Description: 暴力反射破解單例 */ public class ReflectBreakSingletonTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //1,獲取單例對象 HungryTest hungry1 = HungryTest.getInstance(); //2, 獲取HungryTest類字節碼對象 Class<HungryTest> hungryClass= HungryTest.class; //3,獲取構造器對象 Constructor<HungryTest> hungryConstructor = hungryClass.getDeclaredConstructor(); //4,設置暴力反射爲true hungryConstructor.setAccessible(true); //5,經過構造器對象調用默認構造器建立對象 --> 反射 HungryTest hungry2= hungryConstructor.newInstance(); //6, 判斷兩個對象是否相同 System.out.println(hungry1 == hungry2); } }
運行結果: false
證實反射能夠破壞單例對象惟一,新建立對象。
如何防止反射對單例的攻擊?
既然反射攻擊是調用默認構造器,那麼反射在調用構造器時就拋出異常不讓其建立對象。依然以餓漢式爲例,修改默認構造方法,若是反射調用就拋出異常!
private HungryTest() { if(null !=INSTANCE) { throw new RuntimeException("不支持反射調用默認構造器!"); } };
問:以上6種單例模式均可以經過在默認構造方法中拋異常防止暴力反射嗎?
答:除去枚舉(其自然防止反射),其餘5種分爲2類,類初始化就建立對象爲預加載方式,另外一類爲延遲加載方式;餓漢式、靜態內部類爲預加載方式 ,懶漢式、雙檢鎖、雙檢鎖變式爲延遲加載方式。這裏預加載能夠用以上方法防止暴力反射,延遲加載不行,由於在默認構造方法中首先會對單例對象判空,延遲加載在獲取單例時是沒有建立對象的,這時能夠經過反射建立對象,所以沒法防止反射攻擊,所以推薦的是枚舉方式實現單例,省心省力。
參考博客:單例模式的攻擊之反射攻擊
本文從單例模式的幾種方式入手,分析每一個的特色及問題,其中它們公共的特色是私有構造方法,再提供一個公開靜態的方法供外部獲取對象;咱們在理解這幾種方式原理後,可以很容易寫出這些單例,分析每種方式存在的問題,以及改進的方式,其中線程安全問題,反序列化問題,反射問題應着重注意,如此咱們也能較爲全面瞭解單例模式。