一、單例模式
1、什么是單例模式
(1)單例模式
【單例模式(Singleton Pattern):】 定義: Ensure a class has only one instance, and provide a global point of access to it. 直譯:確保一個類只有一個實例,並提供對它的全局訪問點(只允許通過全局訪問點獲取實例對象)。
(2)單例模式實現要點
一般情況下,訪問類中某變量、方法: 可以通過 new 進行對象實例化,再通過 "對象名.變量名"、"對象名.方法名" 的形式獲取。 可以通過 static 修飾(全局)變量、方法,再通過 "類名.變量名"、"類名.方法名" 的形式獲取。可避免使用 new 進行對象實例化。 為了保證一個類只存在一個實例,應該保證其有且只有 一次 實例化 機會: 應該保證其構造方法不能在該類以外的地方被調用(防止使用 new 進行對象實例化)。 構造方法只能在該類中被調用一次。 注: 對象實例化常見方式: new、序列化、克隆、反射。 基本實現要點: 構造方法私有化(防止使用 new 進行對象實例化)。 在類的內部進行一次實例化(構造方法只能在該類中被調用一次)。 對外提供一個全局訪問點(全局變量、全局方法等),可以通過 "類名.變量名" 或者 "類名.方法名" 的形式獲取實例對象(避免使用 new 進行對象實例化)。 注: 反射會破壞 構造方法的私有化,需要注意,后面會介紹。 序列化、克隆 等操作可能會破壞單例模式。需要注意。
(3)使用場景
當頻繁創建、銷毀某個對象時,可以考慮單例模式。
當創建對象消耗資源過多時,但又經常使用時,可以考慮單例模式。
2、常見單例模式實現方式
(1)實現方式
【餓漢式:】 靜態變量 靜態代碼塊 枚舉(推薦) 【懶漢式:】 靜態方法 synchronized 同步方法 synchronized 同步代碼塊 雙重檢查 靜態內部類
(2)餓漢式、懶漢式 區別
【基本區別:】 懶漢式 在需要使用對象的時候才進行實例化操作。 餓漢式 在類加載時完成實例化操作,可能暫時還不用該對象(占用內存)。 【餓漢式:】 核心: 餓漢式借助 JVM 的類加載機制,在 類加載的初始化階段 完成 實例化操作。 類初始化階段 只會執行一次,從而保證實例的唯一性 以及 線程安全。 當類被主動使用時,才會導致類的初始化。而被動使用時,不會導致類的初始化。 主動使用類的方式: 類的 main 方法被調用時。 執行 new 實例化操作時。 訪問靜態變量、靜態方法時。 實例化子類時(先觸發父類初始化)。 反射調用某類時。 JVM 類加載過程可參考: https://www.cnblogs.com/l-y-h/p/13496969.html#_label1_5 【懶漢式:】 核心: 在需要使用對象的時候才進行實例化操作。 多線程環境下,多個線程可能同時使用對象,需要考慮線程安全問題,防止並發訪問生成多個實例。
二、餓漢式
1、實現
(1)基本說明
【核心思路:】 使用 static 關鍵字,借助類加載過程,進行實例的初始化。 使用 private 修飾 構造方法,保證構造方法私有化。 提供一個全局訪問點(類名.變量名 或者 類名.方法名)獲取對象。 【可用方式:】 靜態變量 靜態方法 靜態代碼塊 【優點:】 在類加載的初始化階段完成了實例化,僅加載一次。保證對象的唯一性 以及 線程安全。 【缺點:】 在類加載的初始化階段完成了實例化,沒有實現懶加載(Lazy Loading),可能造成內存的浪費(在不需要使用的時候被創建)。
(2)代碼實現(靜態變量)
public 修飾變量,直接通過 "類名.變量名" 的方式獲取對象。
class HungrySingleton { // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton singleton = new HungrySingleton(); // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { } } public class Test { public static void main(String[] args) { HungrySingleton singleton = HungrySingleton.singleton; HungrySingleton singleton2 = HungrySingleton.singleton; System.out.println(singleton == singleton2); // true,為同一個對象 } }
(3)代碼實現(靜態方法)
private 修飾變量,不允許通過 "類名.變量名" 的形式訪問。
public 修飾方法,通過 "類名.方法名" 的方式獲取對象。
class HungrySingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton = new HungrySingleton(); // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } } public class Test { public static void main(String[] args) { HungrySingleton singleton = HungrySingleton.getInstance(); HungrySingleton singleton2 = HungrySingleton.getInstance(); System.out.println(singleton == singleton2); // true,為同一個對象 } }
(4)代碼實現(靜態代碼塊)
靜態代碼塊,只是將實例化操作 移動到 靜態代碼塊中進行實現。
class HungrySingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行 static { singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } } public class Test { public static void main(String[] args) { HungrySingleton singleton = HungrySingleton.getInstance(); HungrySingleton singleton2 = HungrySingleton.getInstance(); System.out.println(singleton == singleton2); // true,為同一個對象 } }
(5)這就完了嗎?
當然不是了,這樣寫只是防止了通過 new 實例化對象。
對象實例化的方式還有 反射、序列化、克隆 等操作。
這些操作是否會破壞單例模式?需要思考一下。
2、反射破壞
(1)類主動使用時,才會進行類的初始化
類只有主動使用時,才會進行初始化操作。並不一定使用到類,就會觸發初始化操作。
比如:
進行反射獲取私有構造方法時,並不會觸發 類加載過程。
如下代碼執行后,靜態代碼塊中的 "start..." 不會輸出。
import java.lang.reflect.Constructor; class HungrySingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行 static { System.out.println("start..."); singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } } public class Test { public static void main(String[] args) throws Exception { Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class; Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor(); hungrySingletonConstructor.setAccessible(true); } }
(2)反射破壞
如下代碼所示,反射調用構造方法時,會進行類加載過程(輸出 "start..." ),然后構建一個實例。
此時的實例對象是通過 構造方法重新創建的對象。與類加載過程中創建的對象不同。
即 反射對 單例模式造成了破壞。
import java.lang.reflect.Constructor; class HungrySingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行 static { System.out.println("start..."); singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } } public class Test { public static void main(String[] args) throws Exception { Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class; Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor(); hungrySingletonConstructor.setAccessible(true); // 此時,還不會觸發 類加載過程 HungrySingleton singleton = hungrySingletonConstructor.newInstance(); // 此時,觸發 類加載過程,並創建一個實例 HungrySingleton singleton2 = HungrySingleton.getInstance(); System.out.println(singleton == singleton2); // false,不為同一個對象 } }
(3)防止反射破壞(未必會生效)
在構造方法中,判斷實例是否已經被創建。
類初始化過程中,會創建一個實例。即使通過反射調用構造方法,也會在實例創建之后再去調用,所以在 構造方法中進行判斷,實例存在則會拋出異常。從而防止反射破壞(未必會生效,后續序列化破壞中有提到)。
import java.lang.reflect.Constructor; class HungrySingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行 static { System.out.println("start..."); singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { if (singleton != null) { throw new RuntimeException("實例已存在,不允許重復創建"); } } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } } public class Test { public static void main(String[] args) throws Exception { Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class; Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor(); hungrySingletonConstructor.setAccessible(true); HungrySingleton singleton = hungrySingletonConstructor.newInstance(); HungrySingleton singleton2 = HungrySingleton.getInstance(); System.out.println(singleton == singleton2); // true,為同一個對象 } }
3、反序列化破壞
(1)序列化、反序列化
序列化對象,並再次讀取對象時(反序列化),會創建一個新的對象。
注:
序列化就是把實體對象狀態按照一定的格式寫入到有序字節流。
反序列化就是從有序字節流重建對象,恢復對象狀態。
【反序列化核心代碼:】 ObjectInputStream 中的 readOrdinaryObject() 方法 private Object readOrdinaryObject(boolean unshared) throws IOException { Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException(desc.forClass().getName(), "unable to create instance").initCause(ex); } if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (rep != obj) { handles.setObject(passHandle, obj = rep); } } return obj; } 【關注點一:(調用構造函數實例化)】 desc.isInstantiable() 如果一個 serializable/externalizable 的類可以在運行時被實例化,那么該方法就返回true。 desc.newInstance() 通過反射調用無參構造創建一個對象。 注: 此處調用的無參構造,與類本身的無參構造方法有差別。 從實際效果上看,此處僅觸發了類加載,並未觸發類的構造函數。與前面提到的反射有區別。 沒有深入研究,有興趣的可以幫忙解答一下。 【關注點二:(自定義對象生成策略)】 desc.hasReadResolveMethod() 如果一個 serializable/externalizable 接口的類中包含 readResolve() 方法,則返回 true。 desc.invokeReadResolve(obj) 通過反射的方式調用要被反序列化的類的 readResolve() 方法。 handles.setObject(passHandle, obj = rep) 如果 readResolve() 返回的實例與構造方法創建的不同,則以 readResolve() 方法創建的實例為准。
(2)反序列化破壞
如下代碼所示,通過反序列化創建了個對象。
從實際代碼執行結果看,反序列化僅觸發了類加載過程(此時調用了構造函數),反序列化中 newInstance() 未主動觸發類的構造函數,所以此處構造方法中的判斷 無法防止 反序列化中反射的行為。
即 反序列化對 單例模式造成了破壞。
import java.io.FileInputStream; import java.io.ObjectInputStream; import java.io.Serializable; class HungrySingleton implements Serializable { private static final long serialVersionUID = 42L; // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; static { System.out.println("start..."); // start... singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { if (singleton != null) { throw new RuntimeException("實例已存在,不允許重復創建"); } } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } } public class Test { public static void main(String[] args) throws Exception { // ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test")); // oos.writeObject(HungrySingleton.getInstance()); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test")); HungrySingleton singleton = (HungrySingleton) ois.readObject(); HungrySingleton singleton2 = HungrySingleton.getInstance(); System.out.println(singleton == singleton2); // false,不為同一對象 } }
(3)防止反序列化破壞
通過 readResolve() 可以返回一個實例對象,保證此對象為類加載過程中創建的實例對象,即可防止 反序列化破壞。
import java.io.FileInputStream; import java.io.ObjectInputStream; import java.io.Serializable; class HungrySingleton implements Serializable { private static final long serialVersionUID = 42L; // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; static { System.out.println("start..."); // start... singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { if (singleton != null) { throw new RuntimeException("實例已存在,不允許重復創建"); } } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } // 定義 readResolve() 方法,返回類加載過程中創建的實例對象(反序列化時返回此對象) private Object readResolve() { return singleton; } } public class Test { public static void main(String[] args) throws Exception { // ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test")); // oos.writeObject(HungrySingleton.getInstance()); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test")); HungrySingleton singleton = (HungrySingleton) ois.readObject(); HungrySingleton singleton2 = HungrySingleton.getInstance(); System.out.println(singleton == singleton2); // true,為同一對象 } }
4、克隆破壞
(1)克隆破壞
如下代碼所示,通過克隆創建了個對象。
調用了 Object 的 clone 方法(native 方法),與反序列化類似,也沒有觸發 類的構造方法(應該是直接從內存中 copy 了一份)。創建了一個新的對象。
即 克隆對 單例模式造成了破壞。
class HungrySingleton implements Cloneable { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; static { System.out.println("start..."); // start... singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { if (singleton != null) { throw new RuntimeException("實例已存在,不允許重復創建"); } } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } // 重寫 clone() 方法,返回 clone 對象 @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } } public class Test { public static void main(String[] args) throws Exception { HungrySingleton singleton = HungrySingleton.getInstance(); HungrySingleton singleton2 = (HungrySingleton) singleton.clone(); System.out.println(singleton == singleton2); // false,不是同一對象 } }
(2)防止克隆破壞
保證 clone() 方法返回的對象為類加載過程中創建的實例對象,即可防止 克隆破壞。
class HungrySingleton implements Cloneable { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static HungrySingleton singleton; static { System.out.println("start..."); // start... singleton = new HungrySingleton(); } // 構造器私有化(防止通過new創建實例對象) private HungrySingleton() { if (singleton != null) { throw new RuntimeException("實例已存在,不允許重復創建"); } } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static HungrySingleton getInstance() { return singleton; } // 重寫 clone() 方法,返回 clone 對象 @Override public Object clone() throws CloneNotSupportedException { return singleton; } } public class Test { public static void main(String[] args) throws Exception { HungrySingleton singleton = HungrySingleton.getInstance(); HungrySingleton singleton2 = (HungrySingleton) singleton.clone(); System.out.println(singleton == singleton2); // true,是同一對象 } }
5、枚舉
(1)基本說明
【基本說明:】 寫個簡單的 enum 類,然后反編譯一下 javap -c xx.class。 可以看到底層就類似於 餓漢式 靜態代碼塊 的寫法。在類加載的初始化階段完成實例化操作。 【優點:】 在類加載的初始化階段完成了實例化,僅加載一次。保證對象的唯一性 以及 線程安全。 可以防止 克隆、反序列化、反射 破壞單例模式。 寫法簡單。
(2)反編譯一下 enum 類
【EnumSingleton】 enum EnumSingleton { INSTANCE; } 【javap -c EnumSingleton.class】 final class pattern.sington.EnumSingleton extends java.lang.Enum<pattern.sington.EnumSingleton> { public static final pattern.sington.EnumSingleton INSTANCE; public static pattern.sington.EnumSingleton[] values(); Code: 0: getstatic #1 // Field $VALUES:[Lpattern/sington/EnumSingleton; 3: invokevirtual #2 // Method "[Lpattern/sington/EnumSingleton;".clone:()Ljava/lang/Object; 6: checkcast #3 // class "[Lpattern/sington/EnumSingleton;" 9: areturn public static pattern.sington.EnumSingleton valueOf(java.lang.String); Code: 0: ldc #4 // class pattern/sington/EnumSingleton 2: aload_0 3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 6: checkcast #4 // class pattern/sington/EnumSingleton 9: areturn static {}; Code: 0: new #4 // class pattern/sington/EnumSingleton 3: dup 4: ldc #7 // String INSTANCE 6: iconst_0 7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 10: putstatic #9 // Field INSTANCE:Lpattern/sington/EnumSingleton; 13: iconst_1 14: anewarray #4 // class pattern/sington/EnumSingleton 17: dup 18: iconst_0 19: getstatic #9 // Field INSTANCE:Lpattern/sington/EnumSingleton; 22: aastore 23: putstatic #1 // Field $VALUES:[Lpattern/sington/EnumSingleton; 26: return } 【等價於:】 public final class EnumSingleton extends Enum< EnumSingleton> { public static final EnumSingleton INSTANCE; public static EnumSingleton[] values(); public static EnumSingleton valueOf(String s); static { INSTANCE = new EnumSingleton(name, ordinal); }; }
(3)防止反射破壞
枚舉類型的類,沒有無參構造。默認繼承 Enum 的有參構造。
【代碼實現:】 import java.lang.reflect.Constructor; enum EnumSingleton { INSTANCE; } public class Test { public static void main(String[] args) throws Exception { Class<EnumSingleton> enumSingletonClass = EnumSingleton.class; Constructor<EnumSingleton> enumSingletonConstructor = enumSingletonClass.getDeclaredConstructor(String.class, int.class); enumSingletonConstructor.setAccessible(true); // newInstance 會出現異常,java.lang.IllegalArgumentException: Cannot reflectively create enum objects EnumSingleton enumSingleton = enumSingletonConstructor.newInstance(); EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE; System.out.println(enumSingleton == enumSingleton2); } } 【原因分析:】 newInstance() 方法中進行判斷,若為枚舉類型,則拋異常。 @CallerSensitive public T newInstance(Object ... initargs) throws IllegalArgumentException { if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); }
(4)防止克隆破壞
枚舉類型的類,無法重寫 clone() 方法。其父類 Enum 中定義 clone() 方法為 final 類型,不能被子類重寫。
protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
(5)防止序列化破壞
序列化返回的是同一個對象,無需定義 readResolve() 方法。其執行的是另一個邏輯。
【代碼實現:】 import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; enum EnumSingleton { INSTANCE; } public class Test { public static void main(String[] args) throws Exception { EnumSingleton enumSingleton = EnumSingleton.INSTANCE; ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test")); oos.writeObject(enumSingleton); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test")); EnumSingleton enumSingleton2 = (EnumSingleton) ois.readObject(); System.out.println(enumSingleton == enumSingleton2); // true,是同一個對象 } } 【反序列化核心代碼:】 ObjectInputStream 中的 readEnum() 方法。 讀入並返回枚舉常量,如果枚舉類型不可解析,則返回 null。 private Enum<?> readEnum(boolean unshared) throws IOException { ObjectStreamClass desc = readClassDesc(false); int enumHandle = handles.assign(unshared ? unsharedMarker : null); String name = readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } } handles.finish(enumHandle); passHandle = enumHandle; return result; }
三、懶漢式
1、實現
(1)基本說明
【核心思路:】 使用 private 修飾 構造方法,保證構造方法私有化。 提供一個靜態的公共方法,在調用該方法時,才去創建實例對象。(全局訪問點,通過 "類名.方法名" 獲取對象)。 【可用方式:】 靜態方法 synchronized 同步方法 synchronized 同步代碼塊 雙重檢查 靜態內部類 【優點:】 懶加載,需要使用對象時才會去實例化操作,提高內存利用率。 【缺點:】 多線程環境下,多個線程可能同時使用對象,需要考慮線程安全問題,防止並發訪問生成多個實例。
(2)代碼實現(靜態方法)
如下代碼所示,只允許通過 "類名.方法名" 的方式獲取對象。
class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,通過 "類名.變量名" 訪問 public static FullSingleton getInstance() { if (fullSingleton == null) { fullSingleton = new FullSingleton(); } return fullSingleton; } } public class Test { public static void main(String[] args) throws Exception { FullSingleton fullSingleton = FullSingleton.getInstance(); FullSingleton fullSingleton2 = FullSingleton.getInstance(); System.out.println(fullSingleton == fullSingleton2); // true,是同一個對象 } }
(3)這就完了嗎?
當然不是了,這樣寫只是在單線程環境下正常執行。多線程操作下,會出現多個實例。
比如:
線程 A 與線程 B 並發執行到 if (fullSingleton == null),此時兩個線程的 fullSingleton 均為 null,則均會進入方法,執行 new 實例化操作,此時便會產生多個實例對象。
如下代碼所示,代碼執行多次可以發現,兩個線程輸出的對象並不一致。
此時單例模式被破壞,線程不安全。
class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { if (fullSingleton == null) { fullSingleton = new FullSingleton(); } return fullSingleton; } } public class Test { public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@f3f9f4b } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@26af6bb1 } }); thread.start(); thread2.start(); } }
(4)代碼實現(synchronized 同步方法)
為了保證線程安全,可以使用 synchronized 關鍵字實現同步。
注:
synchronized 保證同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊。
如下代碼所示,在方法上添加一個 synchronized,代碼執行多次可以發現,兩個線程輸出的對象始終一致。
class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問 public static synchronized FullSingleton getInstance() { if (fullSingleton == null) { fullSingleton = new FullSingleton(); } return fullSingleton; } } public class Test { public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@40788638 } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@40788638 } }); thread.start(); thread2.start(); } }
(5)這就完了嗎?
當然不是了,雖然使用 synchronized 保證線程安全,但是這種方式鎖粒度太大,可能會導致執行效率低。
(6)代碼實現(synchronized 同步代碼塊)
如下代碼所示,為了縮小 synchronized 影響范圍,可以在方法內部使用同步代碼塊的方式實現。
class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { if (fullSingleton == null) { synchronized(FullSingleton.class) { fullSingleton = new FullSingleton(); } } return fullSingleton; } } public class Test { public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@5eb39c2b } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@401a5cff } }); thread.start(); thread2.start(); } }
(7)這就完了嗎?
當然不是了,這樣寫又回到了 靜態方法中 提到的 線程不安全的問題上了。
比如:
線程 A 與線程 B 並發執行到 if (fullSingleton == null),此時兩個線程的 fullSingleton 均為 null,則均會進入方法,遇到 synchronized,同步執行后,仍會執行 new 操作,產生多個實例對象。
此時單例模式被破壞,線程不安全。雙重檢查可以解決這個問題。
2、雙重檢查
(1)代碼實現
如下代碼所示,雙重檢查,在 synchronized 同步代碼塊 的基礎上,再添加一個判斷。
class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { if (fullSingleton == null) { synchronized(FullSingleton.class) { if (fullSingleton == null) { fullSingleton = new FullSingleton(); } } } return fullSingleton; } } public class Test { public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); } }); thread.start(); thread2.start(); } }
(2)這就完了嗎?
當然不是了。這樣寫看上去是保證了線程安全,但是有個細節需要思考一下(指令重排)。
如下所示,反編譯一下代碼,可以看到實例化操作的相關指令。
【Test.java】 public class Test { public static void main(String[] args) { Test test = new Test(); } } 【javap -c Test.class】 public class pattern.sington.Test { public pattern.sington.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class pattern/sington/Test 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: return } 【關注 main 函數:】 new 指令在 堆內存中為 Test 對象分配內存空間。 invokespecial 指令,執行實例初始化操作。 astore_1 指令,將棧頂引用類型值存入變量(即 使對象指向 堆內存空間)。 即分為三步: 1、分配內存空間。 2、實例初始化 3、實例指向內存空間 注: 按照常理說,1、2、3 是按照順序執行的。 但是 JVM 會根據處理器特性,對指令進行優化(指令重排序),從而提高性能。 指令重排,意味着指令可能不會按照指定順序執行。 【回到上例的 雙重檢查的代碼:】 發生指令重排,new 實例化操作按照 1、3、2 的順序執行。
假設線程 A 執行完 1、3,但 2 還未執行完,即對象已指向內存空間,但是還沒有初始化。
此時線程 B 執行 getInstance() 代碼,由於對象已指向內存空間,判斷對象是否為 null 時返回 false, 跳過 synchronized 代碼塊。
此時線程 B 拿到的實例對象,由於初始化並未完成,使用對象將可能出現錯誤(引用逃逸)。 注: synchronized 並非原子性操作,可能發生指令重排。 使用 voliate 可以通過 內存屏障 禁止指令重排序。
(3)代碼實現(voliate )
使用 voliate 修飾 變量,禁止指令重排序。
class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 // volatile 防止指令重排 private static volatile FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { if (fullSingleton == null) { synchronized(FullSingleton.class) { if (fullSingleton == null) { fullSingleton = new FullSingleton(); } } } return fullSingleton; } } public class Test { public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@79af4c1c } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@79af4c1c } }); thread.start(); thread2.start(); } }
3、靜態內部類
(1)基本說明
靜態內部類是一種結合了 餓漢模式、懶漢模式 優點的實現方式。
【核心思路:】 使用 private 修飾 構造方法,保證構造方法私有化。 在類的內部定義一個靜態內部類(只有被調用時,才會被加載),並在內部類中實例化對象。 提供一個靜態的公共方法,在調用該方法時,調用靜態內部類。(全局訪問點,通過 "類名.方法名" 獲取對象)。 【優點:】 定義內部類,只有在用到的時候才回去加載,實現懶加載。 使用 static 定義內部類,利用 JVM 類加載機制保證 線程安全。
(2)代碼實現
如下代碼所示,定義一個靜態內部類。
class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { return InnerInstance.INSTANCE; } // 定義靜態內部類 private static class InnerInstance { public static final FullSingleton INSTANCE = new FullSingleton(); } } public class Test { public static void main(String[] args) throws Exception { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@2a75ae17 } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@2a75ae17 } }); thread.start(); thread2.start(); } }
(3)這就完了嗎?
當然不是了。反序列化破壞、反射破壞、克隆破壞 的問題同樣存在。
解決方式與 餓漢模式的解決方式類似。
(4)防止序列化破壞
重寫 readResolve() 方法,返回實例對象。
import java.io.*; class FullSingleton implements Serializable { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { return InnerInstance.INSTANCE; } // 定義靜態內部類 private static class InnerInstance { public static final FullSingleton INSTANCE = new FullSingleton(); } private Object readResolve() { return getInstance(); } } public class Test { public static void main(String[] args) throws Exception { FullSingleton fullSingleton = FullSingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test")); oos.writeObject(fullSingleton); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test")); FullSingleton fullSingleton2 = (FullSingleton) ois.readObject(); System.out.println(fullSingleton == fullSingleton2); } }
(5)防止克隆破壞
重寫 clone() 方法,返回實例對象。
class FullSingleton implements Cloneable { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { } // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { return InnerInstance.INSTANCE; } // 定義靜態內部類 private static class InnerInstance { public static final FullSingleton INSTANCE = new FullSingleton(); } @Override public Object clone() throws CloneNotSupportedException { return getInstance(); } } public class Test { public static void main(String[] args) throws Exception { FullSingleton fullSingleton = FullSingleton.getInstance(); FullSingleton fullSingleton2 = (FullSingleton) fullSingleton.clone(); System.out.println(fullSingleton == fullSingleton2); } }
(6)防止反射破壞
在構造方法中,新增一個判斷。
import java.lang.reflect.Constructor; class FullSingleton { // 私有化變量,不可以通過 "類名.變量名" 的形式訪問 private static FullSingleton fullSingleton; // 構造器私有化(防止通過new創建實例對象) private FullSingleton () { if (getInstance() != null) { throw new RuntimeException("實例已存在,創建失敗"); } } // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問 public static FullSingleton getInstance() { return InnerInstance.INSTANCE; } // 定義靜態內部類 private static class InnerInstance { public static final FullSingleton INSTANCE = new FullSingleton(); } } public class Test { public static void main(String[] args) throws Exception { Class<FullSingleton> fullSingletonClass = FullSingleton.class; Constructor<FullSingleton> fullSingletonConstructor = fullSingletonClass.getDeclaredConstructor(); fullSingletonConstructor.setAccessible(true); FullSingleton fullSingleton = fullSingletonConstructor.newInstance(); FullSingleton fullSingleton2 = FullSingleton.getInstance(); System.out.println(fullSingleton == fullSingleton2); } }
四、舉例
1、JDK中的單例模式舉例(Runtime)
(1)部分源碼
如下代碼所示,就是 餓漢模式的 實現。
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } private Runtime() {} }