重點:
1.模式定義/應用場景/類圖分析
2.位元組碼知識/位元組碼指令重排
3.類加載機制
4.JVM序列化機制
5.單例模式在Spring架構 & JDK源碼中的應用
模式定義:保證一個類隻有一個執行個體,并且隻提供一個全局通路點
使用場景:重量級的對象,不需要多個執行個體,如線程池,資料庫連接配接池
單例模式UML類圖
1. 懶漢模式:延遲加載,隻有真正使用的時候,才開始執行個體化
1)線程安全問題
2)double check 加鎖優化
3)JIT編譯器,CPU有可能對指令進行重排,導緻使用到尚未初始化的執行個體,可以通過添加volatile關鍵字修飾解決。即volatile修飾的字段能夠防止指令重排。
單線程環境下的實作:
public class LazyInstance { public static void main(String[] args) { LazySingleton lazySingleton1 = LazySingleton.getInstance(); LazySingleton lazySingleton2 = LazySingleton.getInstance(); System.out.println(lazySingleton2 == lazySingleton1); }}class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }}
多線程環境下全局通路方法getInstance()不加鎖:
public class ConcurrentLazyInstance { public static void main(String[] args) { new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread1instance = " + instance); }).start(); new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread2instance = " + instance); }).start(); }}class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }}
破壞了單例的定義,産生了兩個對象
多線程環境下全局通路方法getInstance()加鎖:
public class ConcurrentLazyInstance { public static void main(String[] args) { new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread1instance = " + instance); }).start(); new Thread(() -> { LazySingleton instance = LazySingleton.getInstance(); System.out.println("Thread2instance = " + instance); }).start(); }}class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public synchronized static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }}
通過synchronize鎖住方法,保證單例,但性能會有所下降
為了提高性能,getInstance()方法我們采用雙重校驗鎖
public static LazySingleton getInstance() { //先判斷對象是否已經執行個體過,沒有執行個體化過才進入加鎖代碼 if (instance == null) { //類對象加鎖 synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; }}
另外,需要注意 instance 采用 volatile 關鍵字修飾也是很有必要。
instance 采用 volatile 關鍵字修飾也是很有必要的, instance = new LazySingleton(); 這段代碼其實是分
為三步執行:
1). 為 instance 配置設定記憶體空間
2). 初始化 instance
3). 将 instance 指向配置設定的記憶體位址
但是由于 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出現問題,但是在
多線程環境下會導緻一個線程獲得還沒有初始化的執行個體。例如,線程 T1 執行了 1 和 3,此時 T2 調用
getInstance() 後發現 instance 不為空,是以傳回 instance,但此時 instance 還未被
初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運作。
- 餓漢式:JVM類加載的初始化階段就完成了執行個體化的初始化。本質上借助類加載機制,保證執行個體的唯一性。
類加載的過程:
1)加載:二進制資料加載到記憶體,生成對應的Class資料結構
2)連接配接:a.驗證,b.準備(給類的靜态成員變量賦預設值),c.解析
3)初始化:給類的靜态變量賦初值
單線程環境下的實作餓漢式:
public class HungrySingletonTest { public static void main(String[] args) { HungrySingleton lazySingleton1 = HungrySingleton.getInstance(); HungrySingleton lazySingleton2 = HungrySingleton.getInstance(); System.out.println(lazySingleton2 == lazySingleton1); }}class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}
- 靜态内部類
1)本質上是利用類的加載機制保證線程線程安全
2)當在實際使用的時候才會觸發類的初始化。是以也是懶加載的一種形式
class InnerClassSingleton { private static class InnerClassHolder { private static InnerClassSingleton instance = new InnerClassSingleton().getInstance(); } private InnerClassSingleton() { } public static InnerClassSingleton getInstance() { return InnerClassHolder.instance; }}
- 反射攻擊
在餓漢模式和靜态内部類初始化情況下,反射執行個體化和單例執行個體化不是一個對象
public class HungrySingletonTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //反射執行個體化和單例執行個體化不是一個對象 //如何防止?在餓漢模式和靜态内部類的初始化構造函數中判斷類的執行個體是否為null,不等于null則抛出異常 Constructor declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance(); InnerClassSingleton instance3 = InnerClassSingleton.getInstance(); System.out.println(instance3==innerClassSingleton); }}class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}class InnerClassSingleton { private static class InnerClassHolder { private static InnerClassSingleton instance = new InnerClassSingleton(); } private InnerClassSingleton() { if(InnerClassHolder.instance != null){ //在餓漢模式和靜态内部類的初始化構造函數中判斷類的執行個體是否為null,不等于null則抛出異常 throw new RuntimeException("單例不允許多個執行個體"); } } public static InnerClassSingleton getInstance() { return InnerClassHolder.instance; }}
如何防止?答:在餓漢模式和靜态内部類的初始化構造函數中判斷類的執行個體是否為null,不等于null則抛出異常
- 枚舉類型
1) 天然不支援反射建立對應執行個體,且有自己的反序列化機制
2) 利用類加載機制保證線程安全
public enum EnumsSingleton { INSTANCE; public void print() { System.out.println("this.hashCode() = " + this.hashCode()); }}class EnumTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { /** * EnumsSingleton instance = EnumsSingleton.INSTANCE; EnumsSingleton instance1 = EnumsSingleton.INSTANCE; System.out.println(instance==instance1); */ Constructor declaredConstructor = EnumsSingleton.class.getDeclaredConstructor(String.class, int.class); declaredConstructor.setAccessible(true); EnumsSingleton enumsSingleton = declaredConstructor.newInstance("INSTANCE", 0); }}
enum天然不支援反射建立對應執行個體
- 通過InnerClassSingleton将instance4執行個體序列化到磁盤上,在項目的根目錄,再從磁盤反序列化到記憶體中,讀出對象,比較放進去的對象和我們讀出來的對象是不是一個對象
public class HungrySingletonTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { //instance4執行個體序列化到磁盤上,在項目的根目錄 InnerClassSingleton instance4 = InnerClassSingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable")); oos.writeObject(instance4); oos.close(); //從磁盤反序列化到記憶體中,讀出對象,比較放進去的對象和我們讀出來的對象是不是一個對象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable")); InnerClassSingleton object = (InnerClassSingleton) ois.readObject(); System.out.println(instance4 == object); }}class HungrySingleton { private static HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}class InnerClassSingleton implements Serializable { private static class InnerClassHolder { private static InnerClassSingleton instance = new InnerClassSingleton(); } private InnerClassSingleton() { if (InnerClassHolder.instance != null) { throw new RuntimeException("單例不允許多個執行個體"); } } public static InnerClassSingleton getInstance() { return InnerClassHolder.instance; }}
這是JVM序列化的一個機制,在源碼中給出了解決方案:在InnerClassSingleton類實作readResolve()方法
Object readResolve() throws ObjectStreamException { return InnerClassHolder.instance; }
為了在序列化反序列化的時候保證資料是一緻的,我們可以在類InnerClassSingleton中加入版本号:
static final long serialVersionUID = 42L;
能夠保證寫入的對象和讀出的對象是同一個對象
類似的,枚舉是天然的保證序列化和反序列化為同一個對象
- 單例在源碼中應用舉例:
Runtime--->有餓漢模式
Currency ---->有享元模式(注意他的反序列化也是用readResolve)
DefaultSingletonBeanRegistry---->有雙重校驗鎖
ReactiveAdapterRegistry---->有雙重校驗鎖
ProxyFactoryBean
TomcatURLStreamHandlerFactory(Tomcat中)