文章目錄
- 0、單例模式
- 1、餓漢式
- 2、懶漢式
- 3、雙重檢查
- 4、靜態內部類
- 5、枚舉
- 6、單例模式的破壞:序列化和反序列化
- 7、單例模式的破壞:反射
- 8、單例模式的實際應用
設計模式即總結出來的一些最佳實現。GoF(四人組) 書中提到23種設計模式,可分為三大類:
- 創建型模式:隱藏了創建對象的過程,通過邏輯方法進行創建對象,使用者不用關注對象的創建細節(對那種屬性很多,創建麻煩的對象尤其好用)
- 結構型模式:主要關注類和對象的組合關系。將類或對象按某種布局組成更大的結構
- 行為型模式:主要關注對象之間的通信與配合
0、單例模式
- 單例模式即在程序中想要保持一個實例對象,讓某個類只有一個實例
- 單例類必須自己創建自己唯一的實例,并對外提供
- 優點:減少了內存開銷
單例模式的實現,有以下幾種思路:
1、餓漢式
- 類加載就會導致該單實例對象被創建
- 通過靜態代碼塊或者靜態變量直接初始化
方式一:靜態成員變量的方式
public class HungrySingleton {private static final HungrySingleton hungrySingleton = new HungrySingleton();//私有的構造方法,只能本類調用,不給外界用private HungrySingleton(){}//提供一個獲取實例的方法給外界public static HungrySingleton getInstance(){return hungrySingleton;}
}
方式二:靜態代碼塊
public class HungrySingleton {private static HungrySingleton hungrySingleton = null;// 靜態代碼塊中進行賦值static {hungrySingleton = new HungrySingleton();}//私有的構造方法,只能本類調用,不給外界用private HungrySingleton(){}//提供一個獲取實例的方法給外界public static HungrySingleton getInstance(){return hungrySingleton;}
}
以上兩種方式,對象會隨著類的加載而創建,如果這個對象后來一直沒被用,就有點白占內存了。
2、懶漢式
類加載不會導致該單實例對象被創建,而是首次使用該對象時才會創建
懶漢式方式一:線程不安全
public class LazySingleton {private static LazySingleton lazySingleton = null;/*** 私有的構造方法,保證出了本類就不能再被調用,以防直接去創建對象*/private LazySingleton() {}/*** 單例對象的獲取*/public static LazySingleton getInstance() {if (lazySingleton == null) {lazySingleton = new LazySingleton();}return lazySingleton;}}
測試類啟兩個線程獲取對象:
public class Test {public static void main(String[] args) {new Thread(() -> {LazySingleton instance = LazySingleton.getInstance();System.out.println(Thread.currentThread().getName() + "-->" + instance);}, "t1").start();new Thread(() -> {LazySingleton instance = LazySingleton.getInstance();System.out.println(Thread.currentThread().getName()+ "-->" + instance);}, "t2").start();}
}
發現可能出現獲取到兩個不同對象的情況,這是因為線程安全問題:
兩個線程A、B,同時執行完
IF 這一行,被掛起,再被喚醒時繼續往下執行,就會創建出兩個不同的實例對象。那最先想到的應該是synchronized關鍵字解決,但這樣性能低下,因為不管對象是否為null,每次都要等著獲取鎖。
//性能低下,一刀切,不可行
public static synchronized LazySingleton getInstance() {if (lazySingleton == null) {lazySingleton = new LazySingleton();}return lazySingleton;}
3、雙重檢查
通過兩個IF判斷,加上同步鎖進行實現。(懶漢式方式二:雙重檢查)
public class DoubleCheckSingleton {private static DoubleCheckSingleton doubleCheckSingleton;/*** 私有的構造方法,保證出了本類就不能再被調用,以防直接去創建對象*/private DoubleCheckSingleton(){}public static DoubleCheckSingleton getInstance(){if(doubleCheckSingleton == null){synchronized (DoubleCheckSingleton.class){if(doubleCheckSingleton == null){doubleCheckSingleton = new DoubleCheckSingleton();}}}return doubleCheckSingleton;}
}
如此,再有A、B兩個線程同時執行到第一個IF,只能有一個成功創建對象,另一個獲取到鎖后,第二重判斷會告訴它已經有對象實例了。而亮點則在于,對象初始化完成后,后面來獲取對象的線程不用等著拿鎖,第一個IF就能告訴它已有對象,不用再等鎖了。
以上雙端檢鎖雖然線程安全,但問題是,JVM指令重排序后,可能出現空指針異常,可再加volatile關鍵字(volatile的可見性和有序性,這里用它的有序性)
//...
private static volatile DoubleCheckSingleton doubleCheckSingleton;
//...
4、靜態內部類
在單例類中,通過私有的靜態內部類,創建單例對象(加private修飾詞的,出了本類無法調用和訪問)。這也是懶漢式的第三種方式。
public class StaticInnerClassSingleton {/*** 私有的靜態內部類實例化對象* 給內部類的屬性賦值一個對象*/private static class InnerClass{private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();}/*** 私有的構造方法,保證出了本類就不能再被調用,以防直接去創建對象*/private StaticInnerClassSingleton(){}/*** 對外提供獲取實例的方法*/public static StaticInnerClassSingleton getInstance(){return InnerClass.staticInnerClassSingleton;}
}
外部類StaticInnerClassSingleton被加載時,其對象不一定被初始化,因為內部類沒有被主動使用到。直到調用getInstance方法時,靜態內部類InnerClass被加載,完成實例化。
靜態內部類在被加載時,不會立即實例化,而是在第一次使用時才會被加載并初始化。
JVM在加載外部類的過程中,不會加載靜態內部類,只有內部類的屬性或方法被調用時才會被加載,并初始化其靜態屬性。 這種延遲加載的特性,使得我們可以通過靜態內部類來實現在需要時創建單例對象。這種方式沒有線程安全問題,也沒有性能影響和空間的浪費。
5、枚舉
- 單例模式的最佳實現方式
- 枚舉類型是線程安全的,并且只會裝載一次
- 可有效防止對單例模式的破壞
- 屬于餓漢式
public enum EnumSingleton {INSTANCE;public static EnumSingleton getInstance(){return INSTANCE;}
}
6、單例模式的破壞:序列化和反序列化
通過流將單例對象,序列化到文件中,然后再反序列化讀取出來。發現通過反序列化方式創建出來的對象內存地址,和原對象不一樣。或者對象序列化到文件后,兩次反序列化得到的對象也不一樣,單例模式被破壞。
public class TestSerializer {public static void main(String[] args) throws Exception {//懶漢式LazySingleton instance = LazySingleton.getInstance();//把對象序列化到文件中ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));oos.writeObject(instance);//從文件中反序列化對象ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));LazySingleton objInstance = (LazySingleton) ois.readObject();System.out.println(instance);System.out.println(objInstance);System.out.println(instance == objInstance);}
}
可以發現單例模式的五種實現方式中,只有枚舉不會被破壞單例模式。如果非要用其他幾種模式,可以加readResolve
方法來重寫反序列化邏輯。因為反序列化創建對象時,是通過反射創建的,反射會調用readResolve方法,并將其返回值做為反序列化的結果。 沒有重寫readResolve方法時,會通過反射創建一個新的對象,從而破壞了單例模式。這一點在對象流ObjectInputStream的源碼可看出:
public class LazySingleton implements Serializable {private static LazySingleton lazySingleton = null;/*** 私有的構造方法,保證出了本類就不能再被調用,以防直接去創建對象*/private LazySingleton() {}/*** 單例對象的獲取*/public static LazySingleton getInstance() {if (lazySingleton == null) {lazySingleton = new LazySingleton();}return lazySingleton;}private Object readResolve(){return lazySingleton;}}
也可考慮使用@JsonCreator注解。
7、單例模式的破壞:反射
- 通過字節碼對象,創建構造器對象
- 通過構造器對象,初始化單例對象
- 由于單例對象的構造方法是private私有的,調用構造器中的方法,賦予權限,創建單例對象
注意私有修飾詞時,反射會IllegalAccessException
處理下private問題,用懶漢模式驗證:
public class TestReflect {public static void main(String[] args) throws Exception{//創建字節碼對象Class<LazySingleton> clazz = LazySingleton.class;//構造器對象Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();//賦予權限constructor.setAccessible(true);//解決了私有化問題,獲取實例對象LazySingleton o1 = constructor.newInstance();//再次反射LazySingleton o2 = constructor.newInstance();System.out.println(o1);System.out.println(o2);System.out.println(o1 == o2);}
}
用枚舉的方式驗證:
public class TestEnumReflect {public static void main(String[] args) throws Exception {Class<EnumSingleton> clazz = EnumSingleton.class;//枚舉下的單例模式,創建構造方法時,需要給兩個參數,藪澤NoSuchMethodException//這兩個參數是源碼中的體現,一個是String,一個是intConstructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true);EnumSingleton instanceReflect = constructor.newInstance("test",1234);EnumSingleton instanceSingleton = EnumSingleton.getInstance();System.out.println(instanceReflect);System.out.println(instanceSingleton);System.out.println(instanceReflect == instanceSingleton);}
}
運行報錯:Cannot reflectively create enum objects
,即反射創建枚舉的單例對象,是不允許的:
在其他單例模式的實現方式里,也可以實現不允許通過反射創建對象,反射靠拿構造方法對象,調整下構造方法(比如雙重檢鎖):
public class DoubleCheckSingleton {private static DoubleCheckSingleton doubleCheckSingleton;/*** 私有的構造方法,保證出了本類就不能再被調用,以防直接去創建對象*/private DoubleCheckSingleton(){if (doubleCheckSingleton != null) {throw new RuntimeException("不允許創建多個對象");}}public static DoubleCheckSingleton getInstance(){if(doubleCheckSingleton == null){synchronized (DoubleCheckSingleton.class){if(doubleCheckSingleton == null){doubleCheckSingleton = new DoubleCheckSingleton();}}}return doubleCheckSingleton;}
}
或者:
8、單例模式的實際應用
JDK的Runtime類就是餓漢式的單例模式:
PS:Runtime類的簡單使用 --> 執行DOS命令并獲取結果
public class RuntimeDemo {public static void main(String[] args) throws IOException {//獲取Runtime類對象Runtime runtime = Runtime.getRuntime();//返回 Java 虛擬機中的內存總量。System.out.println(runtime.totalMemory());//返回 Java 虛擬機試圖使用的最大內存量。System.out.println(runtime.maxMemory());//創建一個新的進程執行指定的字符串命令,返回進程對象Process process = runtime.exec("ipconfig");//獲取命令執行后的結果,通過輸入流獲取InputStream inputStream = process.getInputStream();byte[] arr = new byte[1024 * 1024* 100];//將流輸入到數組,返回讀到的字節個數int b = inputStream.read(arr); //字節數組轉字符串,指定下字符集為GBKSystem.out.println(new String(arr,0 ,b ,"gbk"));}
}