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!

推薦閱讀: