.

什麼是單例

單例模式確保某各類只有一個例項,而且自行例項化並向整個系統提供這個例項。在計算機系統中,執行緒池、快取、日誌物件、對話方塊、印表機、顯示卡的驅動程式物件常被設計成單例。這些應用都或多或少具有資源管理器的功能,每臺計算機可以有若干個印表機,但只能有一個Printer spooler,以避免兩個列印作業同時輸出到印表機中,每臺計算機可以有若干通訊埠,系統應當集中管理這些通訊埠,以避免一個通訊埠同時被兩個請求同時呼叫。總之,選擇單例模式就是為了避免不一致狀態

單例模式特點

1、單例類只能有一個例項。

2、單例類必須自己建立自己唯一的例項。

3、單例類必須給所有其它物件提供這一例項。

單例模式優缺點

1、單例類只有一個例項
2、共享資源,全域性使用
3、節省建立時間,提高效能

單例模式的七種寫法

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

1.餓漢式

package com.xuyu.V1;

/**
 * author:須臾
 */
public class SingletonV1 {
    /**
     * 餓漢式
     *  優點:先天執行緒安全,當類初始化的時候就會建立該物件
     *  缺點:如果餓漢式使用頻繁,可能會影響專案啟動效率
     */
    private static SingletonV1 singletonV1=new SingletonV1();

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

    public static SingletonV1 getInstance(){
        return singletonV1;
    }
    /**
     * 測試單例
     */
    public static void main(String[] args) {
        SingletonV1 instance1 = SingletonV1.getInstance();
        SingletonV1 instance2 = SingletonV1.getInstance();
        //結果為true,說明保證了單例
        System.out.println(instance1==instance2);

    }
}

原始碼分析Runtime

//餓漢式單例
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }
..
}

2.懶漢式(執行緒不安全)

package com.xuyu.V2;

/**
 * author:須臾
 */
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 instance1 = SingletonV2.getInstance();
                    System.out.println(Thread.currentThread().getName()+","+instance1);
                }
            }).start();
        }
    }
}

輸出結果:執行緒不安全

Thread-1,[email protected]
Thread-4,[email protected]
Thread-0,[email protected]
Thread-5,[email protected]
Thread-22,[email protected]
Thread-6,[email protected]
Thread-7,[email protected]
Thread-3,[email protected]
Thread-13,[email protected]
Thread-10,[email protected]
....
Thread-94,[email protected]
Thread-91,[email protected]

3.懶漢式(執行緒安全)

package com.xuyu.V3;

/**
 * author:須臾
 */
public class SingletonV3 {
    /**
     * 懶漢式執行緒安全
     */
    private static SingletonV3 singletonV3;

    private SingletonV3(){}

    /**
     * 效率低
     */
    public synchronized static 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 instance1 = SingletonV3.getInstance();
                    System.out.println(Thread.currentThread().getName()+","+instance1);
                }
            }).start();
        }
    }
}

輸出結果

建立例項SingletonV3
獲取SingletonV3例項
Thread-0,[email protected]
獲取SingletonV3例項
Thread-99,[email protected]
獲取SingletonV3例項
Thread-98,[email protected]
獲取SingletonV3例項
Thread-97,[email protected]
獲取SingletonV3例項
....

4.雙重檢驗鎖(DCL)

package com.xuyu.V4;

public class SingletonV4 {
    /**
     * volatile 禁止指令重排序
     */
    private static volatile SingletonV4 singletonV4;

    private SingletonV4(){}

    public static SingletonV4 getInstance(){
        if(singletonV4==null){//第一次判斷如果沒有建立物件就開始加鎖
            synchronized (SingletonV4.class){
                if (singletonV4==null){//當用戶搶到鎖,判斷初始化
                    System.out.println("第一次開始建立例項物件,獲取到鎖了");
                    try {
                        Thread.sleep(2000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    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 instance1 = SingletonV4.getInstance();
                    System.out.println(Thread.currentThread().getName()+","+instance1);
                }
            }).start();
        }
    }
}

輸出結果:執行緒安全

第一次開始建立例項物件,獲取到鎖了
Thread-99,[email protected]
Thread-89,[email protected]
Thread-92,[email protected]
Thread-91,[email protected]
....
Thread-8,[email protected]
Thread-6,[email protected]
Thread-9,[email protected]
Thread-12,[email protected]
Thread-11,[email protected]3a0ba
Thread-10,[email protected]
Thread-15,[email protected]
Thread-19,[email protected]
Thread-16,[email protected]

5.靜態內部內形式

package com.xuyu.V5;

/**
 * author:須臾
 */
public class SingletonV5 {
    private SingletonV5(){
        System.out.println("物件初始化...");
    }
    public static SingletonV5 getInstance(){
        return SingletonV5Utils.singletonV5;
    }
    /**
     * 靜態內部類方式:能夠避免同步帶來的效率問題和實現懶載入
     */
    public static class SingletonV5Utils{
        private static SingletonV5 singletonV5=new 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.列舉形式

package com.xuyu.V6;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

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

    }

}

輸出結果:反射破壞不了單例

true
Exception in thread "main" java.lang.NoSuchMethodException: com.xuyu.V6.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.xuyu.V6.EnumSingleton.main(EnumSingleton.java:19)

7.使用容器管理

package com.xuyu.V7;

import java.util.HashMap;
import java.util.Map;

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;
}

總結

到這裡七中寫法都介紹完了,至於選擇用哪種形式的單例模式,取決於你的專案本身,是否是有複雜的併發環境,還是需要控制單例物件的資源消耗。