大家好,我是此林。
設計模式為解決特定問題提供了標準化的方法。在項目中合理應用設計模式,可以避免重復解決相同類型的問題,使我們能夠更加專注于具體的業務邏輯,減少重復勞動。設計模式在定義系統結構時通常考慮到未來的擴展。例如,工廠模式、策略模式等能讓系統在增加新功能時無需改動現有代碼,只需擴展新模塊即可,減少了修改現有代碼的風險。
今天分享的是單例模式和模板模式,這兩種設計模式在項目和源碼中的使用。
1. 單例模式
一般開發中,我們使用的是 Spring 框架,默認情況下,我們通過 @Bean
、@Component
等注解注入的 Bean 對象是 單例的(即 Singleton),也就是說?Spring 會在容器啟動時創建一個該類型的 Bean 實例,并在整個應用程序上下文中共享這個實例。可以通過 @Scope
注解來指定 Bean 的作用域,控制其生命周期和作用范圍。默認的作用域是 @Scope("singleton")。
那 Spring 是如何實現單例模式的呢?
關注源碼,發現 Spring 維護了一個全局的單例池(ConcurrentHashMap),key 是 BeanName,value 是 Bean 對象。
(DefaultSingletonBeanRegistry
類實現了 SingletonBeanRegistry
接口)
我們在開發過程中使用Bean對象,會根據 BeanName 去單例池中獲取 Bean 對象,保證了對象的全局的唯一性。
當然,它和我們平時所說的幾種單例模式實現還是不一樣的。
1. 餓漢式
public class Singleton {private static final Singleton instance = new Singleton();private Singleton() {}public static Singleton getSingleton() {return instance;}
}
關鍵點:
1. 使用 private 關鍵字,代表外部無法對變量?instance 直接修改
2. 使用 static 關鍵字,代表?instance
變量在類加載的時候就會被初始化,這個和 JVM 加載類有關。
3. final
關鍵字的作用:
final
用于修飾變量時,表示該變量 一旦賦值就不能再被修改。也就是說,變量 引用 一旦指向某個對象,就不能再指向其他對象。- 當
final
修飾一個引用變量時,它指向的對象不能改變。但是,引用的對象本身是可以改變的,也就是 對象內部的狀態是可以修改的。
4. 私有化構造方法。也就是防止外部通過 new
關鍵字創建多個實例。
5. 最后一個 getSingleton() 方法是提供全局訪問點,返回唯一實例。
6.?在類加載時就初始化實例,避免線程安全問題。缺點是無論是否使用該實例,都會創建一個實例,浪費內存資源。
2. 雙重檢查鎖定(懶加載)
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getSingleton() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;} }
關鍵點:
1. 由于?instance 用了 static 修飾(類級別的變量),且沒有初始化,那么類加載的時候 instance 賦值為 null。
2. 不加 final ,是因為后續 instance 需要的時候會被賦值;如果加了 final ,那么 instance 永遠只能指向 null。當然哈,jdk 是不允許加了 final 的變量為 null 的,會直接編譯錯誤。
3. 加上 volatile 關鍵字,是保證多線程下的內存可見性。即:一個線程修改了 instance 的值,另一個線程馬上就能看到,也就是強制讀主內存,不讀工作內存。
4. 私有化構造方法。同上,防止外部通過 new 創建多個實例。
getSingleton() 詳解:
其實去掉第一個 if 判斷也可以,也就是這樣:
public static Singleton getSingleton() {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}return instance;}
1. 加上第一個 if 的好處是:
無鎖判斷,instance 不為空直接返回,為 null 再加同步鎖,提高性能,避免每次獲取都加鎖。
2. 加了鎖之后為什么還要判斷呢?
試想這么一個場景:兩個線程同時來了,都發現 instance 為 null,線程A先獲取了鎖創建了對象,那么線程B獲取鎖后無需創建對象,所以要在判斷一次是否為 null。
3. 靜態內部類(懶加載)
public class Singleton {private Singleton(){}private static class SingletonHolder {private static final Singleton instance = new Singleton();}public static Singleton getSingleton() {return SingletonHolder.instance;}
}
使用靜態內部類的方式實現單例,JVM 會在加載外部類時延遲加載內部類,既能實現懶加載,又能避免多線程問題。
4. 枚舉類
public enum Singleton {INSTANCE;public void test() {}
}
其他所有的實現單例的方式其實是有問題的,那就是可能被反序列化和反射破壞。
枚舉的寫法的優點:
- 不用考慮懶加載和線程安全的問題,代碼寫法簡潔優雅
- 線程安全
反編譯任何一個枚舉類會發現,枚舉類里的各個枚舉項是是通過static代碼塊來定義和初始化的,它們會在類被加載時完成初始化,而java類的加載由JVM保證線程安全,所以,創建一個Enum類型的枚舉是線程安全的
- 防止破壞單例
我們知道,序列化可以將一個單例的實例對象寫到磁盤,然后再反序列化讀回來,從而獲得一個新的實例。即使構造函數是私有的,反序列化時依然可以通過特殊的途徑去創建類的一個新的實例,相當于調用該類的構造函數。
Java對枚舉的序列化作了規定,在序列化時,僅將枚舉對象的name屬性輸出到結果中,在反序列化時,就是通過java.lang.Enum的valueOf來根據名字查找對象,而不是新建一個新的對象。枚舉在序列化和反序列化時,并不會調用構造方法,這就防止了反序列化導致的單例破壞的問題。
對于反射破壞單例的而言,枚舉類有同樣的防御措施,反射在通過newInstance創建對象時,會檢查這個類是否是枚舉類,如果是,會拋出異常java.lang.IllegalArgumentException: Cannot reflectively create enum objects
,表示反射創建對象失敗。
5. 模板模式
實現模板方法通常有兩步:
1. 抽象類:定義模板方法和抽象方法,在模板方法里會調用抽象方法。
2. 子類:繼承抽象類,重寫抽象方法。子類運行時調用父類的模板方法,模板方法運行時再去調用子類重寫的抽象方法。
源碼應用(AQS,Reentrantlock)
1. AQS 的模板方法定義
AQS 是基礎的抽象類,提供通用的同步機制。它的 acquire() 和 release() 方法是模板方法。AQS 中的 tryAcquire() 和 tryRelease() 抽象方法,定義了獲取鎖和釋放鎖的具體邏輯。
2. ReentrantLock.lock() 源碼
這里?ReentrantLock.lock() 內部就是調用 AQS 的模板方法?acquire(),1 表示要獲取一個鎖,后續 state 會加1。
這是 AQS 的模板方法,其中 tryAcquire(arg) 方法由子類 ReentrantLock 重寫實現。
AQS 作為一個抽象類,除了被 ReentrantLock 繼承,還被 CountDownLatch、Semaphore繼承。所以說,AQS 提供通用的 模板方法,提高了代碼的復用性。