經典設計模式之單例模式

一、什麼是單例模式

單例模式確保一個類只有一個實例,並提供一個全局訪問點,實現單例模式的方法是私有化構造函數,通過getInstance()方法實例化對象,並返回這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。

二、單例模式的特點

  1. 單例類只能有一個實例。
  2. 單例類必須自己創建自己唯一的實例。
  3. 單例類必須給所有其它對象提供這一實例。

三、單例模式的優缺點

優點:

  1. 單例類只有一個實例
  2. 共享資源,全局使用
  3. 節省創建時間,提高性能

缺點:可能存在線程不安全問題。

四、單例模式的七種寫法

分別是「餓漢」、「懶漢(非線程安全)」、「懶漢(線程安全)」、「雙重校驗鎖」、「靜態內部類」、「枚舉」和「容器類管理]

1、餓漢式

public class SingletonV1 {
    /**
     * 餓漢式:
     * 優點:先天線程安全。當類被加載的時候,就會創建對象。
     * 缺點:1、如果項目中使用過多的餓漢式會發生問題,項目在啓動的時候會變的很慢。
     *      2、singletonV1對象是static的,所以會存放在方法區裏面,佔用內存大。
     *      3、如果用戶不使用該對象,也會被提前創建好,比較浪費資源。
     */
    private static SingletonV1 singletonV1 = new SingletonV1();

    /**
     * 將構造函數私有化,禁止初始化
     */
    private SingletonV1(){}

    public static SingletonV1 getInstance(){
        return singletonV1;
    }

    public static void main(String[] args) {
        SingletonV1 v1 = SingletonV1.getInstance();
        SingletonV1 v2 = SingletonV1.getInstance();
        //結果爲true
        System.out.println(v1 == v2);
    }
}
java.lang.Runtime源碼分析:
public class Runtime {	
	private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}

2、懶漢式(線程不安全)

public class SingletonV2 {
    //懶漢式,線程不安全
    private static SingletonV2 singletonV2;

    private SingletonV2(){}

    public static SingletonV2 getInstance(){
        if (singletonV2 == null) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            singletonV2 = new SingletonV2();
        }
        return singletonV2;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                public void run() {
                    SingletonV2 instance2 = SingletonV2.getInstance();
                    System.out.println(Thread.currentThread().getName()+" - "+instance2);
                }
            }).start();
        }
    }
}
結果:線程不安全
Thread-2 - com.xwhy.singleton.v2.SingletonV2@1dda492b
Thread-13 - com.xwhy.singleton.v2.SingletonV2@46360c95
Thread-5 - com.xwhy.singleton.v2.SingletonV2@4e1de1c0
Thread-0 - com.xwhy.singleton.v2.SingletonV2@64403094
Thread-11 - com.xwhy.singleton.v2.SingletonV2@4a86c08f
Thread-12 - com.xwhy.singleton.v2.SingletonV2@5824138f
Thread-24 - com.xwhy.singleton.v2.SingletonV2@66b01c61
Thread-31 - com.xwhy.singleton.v2.SingletonV2@17733f02
Thread-44 - com.xwhy.singleton.v2.SingletonV2@579ade96
Thread-43 - com.xwhy.singleton.v2.SingletonV2@60fe2295
Thread-6 - com.xwhy.singleton.v2.SingletonV2@1b4b1c5b
Thread-49 - com.xwhy.singleton.v2.SingletonV2@1f904935
Thread-3 - com.xwhy.singleton.v2.SingletonV2@4e1de1c0
...

3、懶漢式(線程安全)

public class SingletonV3 {
    //懶漢式,線程不安全
    private static SingletonV3 singletonV3;

    private SingletonV3(){}

    /**
     * 效率低
     * @return
     */
    public static synchronized SingletonV3 getInstance(){
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        if (singletonV3 == null) {
            System.out.println("創建實例singletonV3");
            singletonV3 = new SingletonV3();
        }
        System.out.println("獲取實例singletonV3");
        return singletonV3;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                public void run() {
                    SingletonV3 singletonV3 = SingletonV3.getInstance();
                    System.out.println(Thread.currentThread().getName()+" - "+singletonV3);
                }
            }).start();
        }
    }
}
結果:
創建實例singletonV3
獲取實例singletonV3
Thread-0 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
Thread-99 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
Thread-98 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
Thread-97 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
...

4、雙重檢驗鎖(DCL)

/*
 * 解決懶漢式獲取對象的效率問題
 */
public class SingletonV4 {
    /**
     * volatile 禁止指令重排序,增加可見性
     */
    private volatile static SingletonV4 singletonV4;

    private SingletonV4(){}

    public static SingletonV4 getInstance(){
        //第一次判斷如果沒有創建對象就開始加鎖
        if(singletonV4 == null){
            //當用戶搶到鎖,判斷初始化
            synchronized (SingletonV4.class){
                if(singletonV4 == null){
                    singletonV4 = new SingletonV4();
                }
            }
        }
        return singletonV4;
    }

    /**
     * 測試單例
     */
    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            new Thread(new Runnable() {
                public void run() {
                    SingletonV4 instance4 = SingletonV4.getInstance();
                    System.out.println(Thread.currentThread().getName()+","+instance4);
                }
            }).start();
        }
    }
}
結果:線程安全
Thread-1,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-4,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-5,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-2,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-3,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-7,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-0,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-8,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-6,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-9,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-10,com.xwhy.singleton.v4.SingletonV4@64403094
....

5、靜態內部類

public class SingletonV5 {
    private SingletonV5(){
        System.out.println("構造函數執行...");
    }

    /**
     * 靜態內部類特徵:繼承懶漢式和餓漢式優點,同時解決雙重檢驗鎖的第一次加載慢的問題
     * 讀和寫都不需要同步所以效率非常高
     * 內部類被調用的時候纔會去初始化singletonV5
     */
    private static class SingletonHolder{
        private static final SingletonV5 singletonV5 = new SingletonV5();
    }

    public static SingletonV5 getInstance(){
        return SingletonHolder.singletonV5;
    }

    public static void main(String[] args) {
        System.out.println("項目啓動成功。。。");
        SingletonV5 instance1 = SingletonV5.getInstance();
        SingletonV5 instance2 = SingletonV5.getInstance();
        System.out.println(instance1==instance2);
    }
}
結果:
項目啓動成功。。。
對象初始化...
true

6、枚舉形式

public enum  SingletonEnum {
    INSTANCE;
    // 枚舉能夠絕對有效的防止實例化多次,和防止反射和序列化破壞
    public void add() {
        System.out.println("add方法...");
    }
    /**
     * 測試單例
     */
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SingletonEnum instance1 = SingletonEnum.INSTANCE;
        SingletonEnum instance2 = SingletonEnum.INSTANCE;
        System.out.println(instance1==instance2);
        Constructor<SingletonEnum> declaredConstructor = SingletonEnum.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        SingletonEnum v6 = declaredConstructor.newInstance();
        System.out.println(v6==instance1);

    }
}
輸出結果:
true
Exception in thread "main" java.lang.NoSuchMethodException: com.xwhy.singleton.v6.SingletonEnum.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.xwhy.singleton.v6.SingletonEnum.main(SingletonEnum.java:24)

枚舉反射破解不了單例

7、使用容器管理

public class SingletonManager {
    private static Map<String, Object> objMap = new HashMap<String, Object>();
    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }
    public static Object getService(String key) {
        {
            return objMap.get(key);
        }
    }
}

這種使用SingletonManager 將多種單例類統一管理,在使用時根據key獲取對象對應類型的對象。這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使用成本,也對用戶隱藏了具體實現,降低了耦合度。

五、如何防止破壞單例

雖然單例通過私有構造函數,可以實現防止程序員初始化對象,但是還可以通過反射和序列化技術破壞單例。

1、使用反射技術破壞單例

// 1. 使用懶漢式創建對象
SingletonV3 instance1 = SingletonV3.getInstance();
// 2. 使用Java反射技術初始化對象 執行無參構造函數
Constructor<SingletonV3> declaredConstructor = SingletonV3.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
SingletonV3 instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);

如何防止被反射破壞

私有構造函數
private SingletonV3() throws Exception {
    synchronized (SingletonV3.class) {
        if (singletonV3 != null) {
            throw new Exception("該對象已經初始化..");
        }
        System.out.println("執行SingletonV3無參構造函數...");
    }
}

2、使用序列化技術破壞單例

Singleton instance = Singleton.getInstance();
FileOutputStream fos = new FileOutputStream("E:\\code\\Singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("E:\\code\\Singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleton2 = (Singleton) ois.readObject();
System.out.println(singleton2==instance)

//返回序列化獲取對象 ,保證爲單例
public Object readResolve() {
    return singletonV3;
}

六、枚舉單例爲什麼不能夠反射初始化

枚舉底層原理

1、首先如果使用java反射機制破壞單例,報錯
枚舉反射報錯
通過該錯誤說明,枚舉類中沒有無參的構造函數

2、使用java反編譯技術,查看枚舉類
枚舉類反編譯
從該圖可以看出,枚舉底層其實是一個類

3、枚舉類底層原理分析
使用靜態代碼塊的方式,當靜態代碼塊執行當時候初始化對象,從而可以讓開發者可以通過EnumSingleton.INSTANCE使用。靜態代碼塊
4、在該反編譯源碼中,定義了一個類繼承了Enum 該類是中沒有無參構造函數,所以反射機制調用無參構造函數是無法初始化的。

5、在該類中有一個只有一個有參構造函數
有參構造函數
調用父類構造構造函數
父類構造函數
Name 爲定義調用對象名稱,定義調用對象序號ordinal

6、使用注入有參構造函數是否可以破壞枚舉呢?也不行的。

Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingleton v3 = declaredConstructor.newInstance();
System.out.println(v3 == instance1);

報錯信息
爲什麼報這個錯誤呢?主要原因是 java的反射初始化對象中,只要對象是枚舉類型是不會初始化的。源代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章