Java設計模式之單例模式示例詳解
0.概述
為什麼要使用單例模式?
在我們的系統中,有一些對象其實我們隻需要一個,比如說:線程池、緩存、對話框、註冊表、日志對象、充當打印機、顯卡等設備驅動程序的對象。事實上,這一類對象隻能有一個實例,如果制造出多個實例就可能會導致一些問題的產生,比如:程序的行為異常、資源使用過量、或者不一致性的結果。因此這裡需要用到單例模式
使用單例模式的好處?
對於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級對象而言,是非常可觀的一筆系統開銷
由於new 操作的次數減少,因而對系統內存的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間
1.餓漢式
1.1 餓漢式單例實現
實例會提前創建:
/** * 餓漢式 * * @author xppll * @date 2021/12/24 21:21 */ public class Singleton1 implements Serializable { //構造私有 private Singleton1() { System.out.println("private Singleton1()"); } //唯一實例 private static final Singleton1 INSTANCE = new Singleton1(); //獲得實例方法 public static Singleton1 getINSTANCE() { return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) { //觸發Singleton1類的初始化,會為類的靜態變量賦予正確的初始值,單例對象就會被創建! Singleton1.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton1.getINSTANCE()); System.out.println(Singleton1.getINSTANCE()); } } //輸出: private Singleton1() otherMethod() ----------------------------------- singleton.Singleton1@10bedb4 singleton.Singleton1@10bedb4
1.2 破壞單例的幾種情況
1.反射破壞單例
2.反序列化破壞單例
3.Unsafe破壞單例
演示:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { //觸發Singleton1類的初始化,會為類的靜態變量賦予正確的初始值,單例對象就會被創建! Singleton1.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton1.getINSTANCE()); System.out.println(Singleton1.getINSTANCE()); //反射破壞單例 reflection(Singleton1.class); //反序列化破壞單例 serializable(Singleton1.getINSTANCE()); //Unsafe破壞單例 unsafe(Singleton1.class); } //反射破壞單例 private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { //得到無參 Constructor<?> constructor = clazz.getDeclaredConstructor(); //將此對象的 accessible 標志設置為指示的佈爾值,即設置其可訪問性 constructor.setAccessible(true); //創建實例 System.out.println("反射創建實例:" + constructor.newInstance()); } //反序列化破壞單例 private static void serializable(Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); //序列化 oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); //反序列化 System.out.println("反序列化創建示例:" + ois.readObject()); } //Unsafe破壞單例 private static void unsafe(Class<?> clazz) throws InstantiationException { Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz); System.out.println("Unsafe 創建實例:" + o); } }
結果:
可以看出三種方式都會破壞單例!
1.3 預防單例的破壞
預防反射破壞單例
在構造方法中加個判斷即可:
//構造私有 private Singleton1() { //防止反射破壞單例 if(INSTANCE!=null){ throw new RuntimeException("單例對象不能重復創建"); } System.out.println("private Singleton1()"); }
預防反序列化破壞單例
在Singleton1()
中重寫readResolve
方法:
//重寫這個方法,如果序列化瞭,就會返回這個,不會返回反序列化的對象 public Object readResolve(){ return INSTANCE; }
Unsafe破壞單例無法預防
2.枚舉餓漢式
2.1 枚舉單例實現
枚舉實現單例:
/** * 枚舉實現單例 * * @author xppll * @date 2021/12/24 22:23 */ public enum Singleton2 { INSTANCE; //枚舉的構造方法默認是private的,可以不寫 Singleton2() { System.out.println("private Singleton2()"); } //重寫toString方法 @Override public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } //獲得實例方法(這個可以不要,枚舉變量都是public的) public static Singleton2 getInstance() { return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { //觸發Singleton2類的初始化,會為類的靜態變量賦予正確的初始值,單例對象就會被創建! Singleton2.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton2.getInstance()); System.out.println(Singleton2.getInstance()); } } //輸出: private Singleton2() otherMethod() ----------------------------------- singleton.Singleton2@2de80c singleton.Singleton2@2de80c
可以看出當調用otherMethod()
時,就會觸發類的加載,枚舉對象就會創建,所以枚舉實現單例是餓漢式的
2.2 破壞單例
枚舉類實現單例的好處:
1.反序列化無法破壞枚舉單例
2.反射無法破壞枚舉單例
栗子:
需要先修改反射破壞代碼,枚舉需要有參構造
public class TestSingleton { public static void main(String[] args) throws Exception { Singleton5.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); System.out.println(Singleton5.getInstance()); System.out.println(Singleton5.getInstance()); //反序列化破壞單例 serializable(Singleton2.getInstance()); //Unsafe破壞單例 unsafe(Singleton2.class); //反射破壞單例 reflection(Singleton2.class); } private static void unsafe(Class<?> clazz) throws InstantiationException { Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz); System.out.println("Unsafe 創建實例:" + o); } private static void serializable(Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); System.out.println("反序列化創建實例:" + ois.readObject()); } private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); System.out.println("反射創建實例:" + constructor.newInstance()); } }
結果:
可以看出
1.反射是無法創建枚舉對象!無法破壞枚舉單例
2.反序列化也不會破壞枚舉單例!
3.Unsafe依然會破壞!
3.懶漢式
實現代碼如下:
/** * 懶漢式 * * @author xppll * @date 2021/12/25 08:34 */ public class Singleton3 implements Serializable { //構造私有 private Singleton3() { System.out.println("private Singleton3()"); } //唯一實例 private static Singleton3 INSTANCE = null; public static Singleton3 getInstance() { //第一次調用的時候才創建 if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { Singleton3.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton3.getInstance()); System.out.println(Singleton3.getInstance()); } }
結果:
可以看出隻有在第一次調用getInstance()
時才會創建唯一的單例對象,因此是懶漢式的。
但是這種方式在多線程環境下是會有問題的,可能多個線程會同時執行INSTANCE = new Singleton3();
。因此這裡需要在getInstance()
方法上加上synchronized
關鍵字保證多線程下的正確性:
public static synchronized Singleton3 getInstance() { //第一次調用的時候才創建 if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; }
但是這種方法是有問題的,第一次創建完對象後,以後的操作是不需要在加鎖的,所以這種方式會影響性能!
我們的目標應該是第一次創建單例的時候給予保護,後續操作則不需要加鎖保護!
4.雙檢鎖懶漢式
針對上面的問題,這裡給出第四種方法雙檢鎖懶漢式進行優化:
/** * 雙檢鎖懶漢式 * * @author xppll * @date 2021/12/25 08:53 */ public class Singleton4 { //構造私有 private Singleton4() { System.out.println("private Singleton4()"); } //唯一實例 //這裡volatile的作用是保證共享變量有序性! private static volatile Singleton4 INSTANCE = null; //雙檢鎖優化 public static synchronized Singleton4 getInstance() { //實例沒創建,才會進入內部的 synchronized 代碼塊,提高性能,防止每次都加鎖 if (INSTANCE == null) { //可能第一個線程在synchronized 代碼塊還沒創建完對象時,第二個線程已經到瞭這一步,所以裡面還需要加上判斷 synchronized (Singleton4.class) { //也許有其他線程已經創建實例,所以再判斷一次 if (INSTANCE == null) { INSTANCE = new Singleton4(); } } } return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
5.內部類懶漢式
內部類懶漢式單例實現:
/** * 內部類懶漢式 * * @author xppll * @date 2021/12/25 09:24 */ public class Singleton5 { //構造私有 private Singleton5() { System.out.println("private Singleton5()"); } //靜態內部類實現懶漢式單例,靜態變量的創建會放在靜態代碼塊裡執行,jvm會保證其線程安全 //隻有第一次用到內部類時,才會初始化創建單例 private static class Holder { static Singleton5 INSTANCE = new Singleton5(); } //獲得實例方法 public static Singleton5 getInstance() { return Holder.INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { Singleton5.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton5.getInstance()); System.out.println(Singleton5.getInstance()); } }
結果:
可以看出內部類實現單例也是懶漢式的!
6.JDK中單例的體現
Runtime 體現瞭餓漢式單例
System類下的Console 體現瞭雙檢鎖懶漢式單例
Collections 中的 EmptyNavigableSet內部類懶漢式單例
Collections 中的ReverseComparator.REVERSE_ORDER 內部類懶漢式單例
Comparators.NaturalOrderComparator.INSTANCE 枚舉餓漢式單例
到此這篇關於Java設計模式之單例模式示例詳解的文章就介紹到這瞭,更多相關Java單例模式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!