1. 程式人生 > >設計模式 | 單例模式及典型應用

設計模式 | 單例模式及典型應用

單例是最常見的設計模式之一,實現的方式非常多,同時需要注意的問題也非常多。

本文主要內容:

  • 介紹單例模式
  • 介紹單例模式的N中寫法
  • 單例模式的安全性
    • 序列化攻擊
    • 反射攻擊
  • 單例模式總結
  • 介紹單例模式的典型應用

單例模式

單例模式(Singleton Pattern):確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項,這個類稱為單例類,它提供全域性訪問的方法。單例模式是一種物件建立型模式。

單例模式有三個要點:

  1. 構造方法私有化;
  2. 例項化的變數引用私有化;
  3. 獲取例項的方法共有

角色

Singleton(單例):在單例類的內部實現只生成一個例項,同時它提供一個靜態的 getInstance() 工廠方法,讓客戶可以訪問它的唯一例項;為了防止在外部對其例項化,將其建構函式設計為私有;在單例類內部定義了一個 Singleton 型別的靜態物件,作為外部共享的唯一例項。

單例模式的七種寫法

1、餓漢式

// 執行緒安全
public class Singleton {

    private final static Singleton INSTANCE = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return INSTANCE;
    }
}

優點:簡單,使用時沒有延遲;在類裝載時就完成例項化,天生的執行緒安全

缺點:沒有懶載入,啟動較慢;如果從始至終都沒使用過這個例項,則會造成記憶體的浪費。

2、餓漢式變種

// 執行緒安全
public class Singleton {

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

將類例項化的過程放在了靜態程式碼塊中,在類裝載的時執行靜態程式碼塊中的程式碼,初始化類的例項。優缺點同上。

3、懶漢式

// 執行緒不安全
public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

優點:懶載入,啟動速度快、如果從始至終都沒使用過這個例項,則不會初始化該實力,可節約資源

缺點:多執行緒環境下執行緒不安全。if (singleton == null) 存在競態條件,可能會有多個執行緒同時進入 if 語句,導致產生多個例項

4、懶漢式變種

// 執行緒安全,效率低
public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

優點:解決了上一種實現方式的執行緒不安全問題

缺點:synchronized 對整個 getInstance() 方法都進行了同步,每次只有一個執行緒能夠進入該方法,併發效能極差

5、雙重檢查鎖

// 執行緒安全
public class Singleton {
    // 注意:這裡有 volatile 關鍵字修飾
    private static volatile Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

優點:執行緒安全;延遲載入;效率較高。

由於 JVM 具有指令重排的特性,在多執行緒環境下可能出現 singleton 已經賦值但還沒初始化的情況,導致一個執行緒獲得還沒有初始化的例項。volatile 關鍵字的作用:

  • 保證了不同執行緒對這個變數進行操作時的可見性
  • 禁止進行指令重排序

6、靜態內部類

// 執行緒安全
public class Singleton {

    private Singleton() {}

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

優點:避免了執行緒不安全,延遲載入,效率高。

靜態內部類的方式利用了類裝載機制來保證執行緒安全,只有在第一次呼叫getInstance方法時,才會裝載SingletonInstance內部類,完成Singleton的例項化,所以也有懶載入的效果。

加入引數 -verbose:class 可以檢視類載入順序

$ javac Singleton.java
$ java -verbose:class Singleton

7、列舉

// 執行緒安全
public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

優點:通過JDK1.5中新增的列舉來實現單例模式,寫法簡單,且不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件

單例模式的安全性

單例模式的目標是,任何時候該類都只有唯一的一個物件。但是上面我們寫的大部分單例模式都存在漏洞,被攻擊時會產生多個物件,破壞了單例模式。

序列化攻擊

通過Java的序列化機制來攻擊單例模式

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton singleton = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(singleton); // 序列化

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
        HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); // 反序列化

        System.out.println(singleton);
        System.out.println(newSingleton);
        System.out.println(singleton == newSingleton);
    }
}

結果

[email protected]
[email protected]
false

Java 序列化是如何攻擊單例模式的呢?我們需要先複習一下Java的序列化機制

Java 序列化機制

java.io.ObjectOutputStream 是Java實現序列化的關鍵類,它可以將一個物件轉換成二進位制流,然後可以通過 ObjectInputStream 將二進位制流還原成物件。具體的序列化過程不是本文的重點,在此僅列出幾個要點。

Java 序列化機制的要點

  • 需要序列化的類必須實現java.io.Serializable介面,否則會丟擲NotSerializableException異常
  • 若沒有顯示地宣告一個serialVersionUID變數,Java序列化機制會根據編譯時的class自動生成一個serialVersionUID作為序列化版本比較(驗證一致性),如果檢測到反序列化後的類的serialVersionUID和物件二進位制流的serialVersionUID不同,則會丟擲異常
  • Java的序列化會將一個類包含的引用中所有的成員變數儲存下來(深度複製),所以裡面的引用型別必須也要實現java.io.Serializable介面
  • 當某個欄位被宣告為transient後,預設序列化機制就會忽略該欄位,反序列化後自動獲得0或者null值
  • 靜態成員不參與序列化
  • 每個類可以實現readObjectwriteObject方法實現自己的序列化策略,即使是transient修飾的成員變數也可以手動呼叫ObjectOutputStreamwriteInt等方法將這個成員變數序列化。
  • 任何一個readObject方法,不管是顯式的還是預設的,它都會返回一個新建的例項,這個新建的例項不同於該類初始化時建立的例項
  • 每個類可以實現private Object readResolve()方法,在呼叫readObject方法之後,如果存在readResolve方法則自動呼叫該方法,readResolve將對readObject的結果進行處理,而最終readResolve的處理結果將作為readObject的結果返回。readResolve的目的是保護性恢復物件,其最重要的應用就是保護性恢復單例、列舉型別的物件
  • Serializable介面是一個標記介面,可自動實現序列化,而Externalizable繼承自Serializable,它強制必須手動實現序列化和反序列化演算法,相對來說更加高效
序列化破壞單例模式的解決方案

根據上面對Java序列化機制的複習,我們可以自定義一個 readResolve,在其中返回類的單例物件,替換掉 readObject 方法反序列化生成的物件,讓我們自己寫的單例模式實現保護性恢復物件

public class HungrySingleton implements Serializable {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }

    private Object readResolve() {
        return instance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton singleton = HungrySingleton.getInstance();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
        HungrySingleton newSingleton = (HungrySingleton) ois.readObject();

        System.out.println(singleton);
        System.out.println(newSingleton);
        System.out.println(singleton == newSingleton);
    }
}

再次執行

[email protected]
[email protected]
true

注意:自己實現的單例模式都需要避免被序列化破壞

反射攻擊

在單例模式中,構造器都是私有的,而反射可以通過構造器物件呼叫 setAccessible(true) 來獲得許可權,這樣就可以建立多個物件,來破壞單例模式了

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        HungrySingleton instance = HungrySingleton.getInstance();
        Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);    // 獲得許可權
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

輸出結果

[email protected]
[email protected]
false
反射攻擊解決方案

反射是通過它的Class物件來呼叫構造器建立新的物件,我們只需要在構造器中檢測並丟擲異常就可以達到目的了

private HungrySingleton() {
    // instance 不為空,說明單例物件已經存在
    if (instance != null) {
        throw new RuntimeException("單例模式禁止反射呼叫!");
    }
}

執行結果

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.singleton.HungrySingleton.main(HungrySingleton.java:32)
Caused by: java.lang.RuntimeException: 單例模式禁止反射呼叫!
	at com.singleton.HungrySingleton.<init>(HungrySingleton.java:20)
	... 5 more

注意,上述方法針對餓漢式單例模式是有效的,但對懶漢式的單例模式是無效的,懶漢式的單例模式是無法避免反射攻擊的!

為什麼對餓漢有效,對懶漢無效?因為餓漢的初始化是在類載入的時候,反射一定是在餓漢初始化之後才能使用;而懶漢是在第一次呼叫 getInstance() 方法的時候才初始化,我們無法控制反射和懶漢初始化的先後順序,如果反射在前,不管反射建立了多少物件,instance都將一直為null,直到呼叫 getInstance()

事實上,實現單例模式的唯一推薦方法,是使用列舉類來實現。

為什麼推薦使用列舉單例

寫下我們的列舉單例模式

package com.singleton;

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

public enum SerEnumSingleton implements Serializable {
    INSTANCE;   // 單例物件
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    private SerEnumSingleton() {
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SerEnumSingleton singleton1 = SerEnumSingleton.INSTANCE;
        singleton1.setContent("列舉單例序列化");
        System.out.println("列舉序列化前讀取其中的內容:" + singleton1.getContent());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
        oos.writeObject(singleton1);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerEnumSingleton singleton2 = (SerEnumSingleton) ois.readObject();
        ois.close();
        System.out.println(singleton1 + "\n" + singleton2);
        System.out.println("列舉序列化後讀取其中的內容:" + singleton2.getContent());
        System.out.println("列舉序列化前後兩個是否同一個:" + (singleton1 == singleton2));

        Constructor<SerEnumSingleton> constructor = SerEnumSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        SerEnumSingleton singleton3 = constructor.newInstance(); // 通過反射建立物件
        System.out.println("反射後讀取其中的內容:" + singleton3.getContent());
        System.out.println("反射前後兩個是否同一個:" + (singleton1 == singleton3));
    }
}

執行結果,序列化前後的物件是同一個物件,而反射的時候丟擲了異常

列舉序列化前讀取其中的內容:列舉單例序列化
INSTANCE
INSTANCE
列舉序列化後讀取其中的內容:列舉單例序列化
列舉序列化前後兩個是否同一個:true
Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.SerEnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.singleton.SerEnumSingleton.main(SerEnumSingleton.java:39)

編譯後,再通過 JAD 進行反編譯得到下面的程式碼

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   SerEnumSingleton.java

package com.singleton;

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

public final class SerEnumSingleton extends Enum
    implements Serializable
{

    public static SerEnumSingleton[] values()
    {
        return (SerEnumSingleton[])$VALUES.clone();
    }

    public static SerEnumSingleton valueOf(String name)
    {
        return (SerEnumSingleton)Enum.valueOf(com/singleton/SerEnumSingleton, name);
    }

    public String getContent()
    {
        return content;
    }

    public void setContent(String content)
    {
        this.content = content;
    }

    private SerEnumSingleton(String s, int i)
    {
        super(s, i);
    }

    public static void main(String args[])
        throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException
    {
        SerEnumSingleton singleton1 = INSTANCE;
        singleton1.setContent("\u679A\u4E3E\u5355\u4F8B\u5E8F\u5217\u5316");
        System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton1.getContent()).toString());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
        oos.writeObject(singleton1);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerEnumSingleton singleton2 = (SerEnumSingleton)ois.readObject();
        ois.close();
        System.out.println((new StringBuilder()).append(singleton1).append("\n").append(singleton2).toString());
        System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton2.getContent()).toString());
        System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton2).toString());
        Constructor constructor = com/singleton/SerEnumSingleton.getDeclaredConstructor(new Class[0]);
        constructor.setAccessible(true);
        SerEnumSingleton singleton3 = (SerEnumSingleton)constructor.newInstance(new Object[0]);
        System.out.println((new StringBuilder()).append("\u53CD\u5C04\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton3.getContent()).toString());
        System.out.println((new StringBuilder()).append("\u53CD\u5C04\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton3).toString());
    }

    public static final SerEnumSingleton INSTANCE;
    private String content;
    private static final SerEnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new SerEnumSingleton("INSTANCE", 0);
        $VALUES = (new SerEnumSingleton[] {
            INSTANCE
        });
    }
}

通過反編譯後代碼我們可以看到,ublic final class T extends Enum,說明,當我們使用enmu來定義一個列舉型別的時候,編譯器會自動幫我們建立一個final型別的類繼承Enum類,所以列舉型別不能被繼承。

那麼,為什麼推薦使用列舉單例呢?

1. 列舉單例寫法簡單

2. 執行緒安全&懶載入

程式碼中 INSTANCE 變數被 public static final 修飾,因為static型別的屬性是在類載入之後初始化的,JVM可以保證執行緒安全;且Java類是在引用到的時候才進行類載入,所以列舉單例也有懶載入的效果。

3. 列舉自己能避免序列化攻擊

為了保證列舉型別像Java規範中所說的那樣,每一個列舉型別極其定義的列舉變數在JVM中都是唯一的,在列舉型別的序列化和反序列化上,Java做了特殊的規定。

在序列化的時候Java僅僅是將列舉物件的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查詢列舉物件。同時,編譯器是不允許任何對這種序列化機制的定製,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我們看一下Enum類的valueOf方法:

    public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

從程式碼中可以看到,程式碼會嘗試從呼叫enumType這個Class物件的enumConstantDirectory()方法返回的map中獲取名字為name的列舉物件,如果不存在就會丟擲異常。再進一步跟到enumConstantDirectory()方法,就會發現到最後會以反射的方式呼叫enumType這個型別的values()靜態方法,也就是上面我們看到的編譯器為我們建立的那個方法,然後用返回結果填充enumType這個Class物件中的enumConstantDirectory屬性。所以,JVM對序列化有保證。

4. 列舉能夠避免反射攻擊,因為反射不支援建立列舉物件

Constructor類的 newInstance方法中會判斷是否為 enum,若是會丟擲異常

    @CallerSensitive
    public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 不能為 ENUM,否則丟擲異常:不能通過反射建立 enum 物件
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

單例模式總結

單例模式作為一種目標明確、結構簡單、理解容易的設計模式,在軟體開發中使用頻率相當高,在很多應用軟體和框架中都得以廣泛應用。

單例模式的主要優點

  • 單例模式提供了對唯一例項的受控訪問。
  • 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件,單例模式可以提高系統的效能。
  • 允許可變數目的例項。基於單例模式我們可以進行擴充套件,使用與單例控制相似的方法來獲得指定個數的物件例項,既節省系統資源,又解決了單例單例物件共享過多有損效能的問題。

單例模式的主要缺點

  • 由於單例模式中沒有抽象層,因此單例類的擴充套件有很大的困難。
  • 單例類的職責過重,在一定程度上違背了 “單一職責原則”。
  • 如果例項化的共享物件長時間不被利用,系統可能會認為它是垃圾,會自動銷燬並回收資源,下次利用時又將重新例項化,這將導致共享的單例物件狀態的丟失。

適用場景

  • 系統只需要一個例項物件,如系統要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許建立一個物件。
  • 客戶呼叫類的單個例項只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該例項。

單例模式的典型應用

JDK Runtime 餓漢單例

JDK Runtime類代表著Java程式的執行時環境,每個Java程式都有一個Runtime例項,該類會被自動建立,我們可以通過 Runtime.getRuntime() 方法來獲取當前程式的Runtime例項。一旦得到了一個當前的Runtime物件的引用,就可以呼叫Runtime物件的方法去控制Java虛擬機器的狀態和行為。

Runtime 應用了餓漢式單例模式

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {
    }
    //....
}

API 介紹

addShutdownHook(Thread hook) 註冊新的虛擬機器來關閉掛鉤。 
availableProcessors() 向 Java 虛擬機器返回可用處理器的數目。 
exec(String command) 在單獨的程序中執行指定的字串命令。 
exec(String[] cmdarray) 在單獨的程序中執行指定命令和變數。 
exec(String[] cmdarray, String[] envp) 在指定環境的獨立程序中執行指定命令和變數。 
exec(String[] cmdarray, String[] envp, File dir) 在指定環境和工作目錄的獨立程序中執行指定的命令和變數。 
exec(String command, String[] envp) 在指定環境的單獨程序中執行指定的字串命令。 
exec(String command, String[] envp, File dir) 在有指定環境和工作目錄的獨立程序中執行指定的字串命令。 
exit(int status) 通過啟動虛擬機器的關閉序列,終止當前正在執行的 Java 虛擬機器。 
freeMemory() 返回 Java 虛擬機器中的空閒記憶體量。 
gc() 執行垃圾回收器。  
getRuntime() 返回與當前 Java 應用程式相關的執行時物件。 
halt(int status) 強行終止目前正在執行的 Java 虛擬機器。 
load(String filename) 載入作為動態庫的指定檔名。 
loadLibrary(String libname) 載入具有指定庫名的動態庫。 
maxMemory() 返回 Java 虛擬機器試圖使用的最大記憶體量。 
removeShutdownHook(Thread hook) 取消註冊某個先前已註冊的虛擬機器關閉掛鉤。 
runFinalization() 執行掛起 finalization 的所有物件的終止方法。 
totalMemory() 返回 Java 虛擬機器中的記憶體總量。 
traceInstructions(on) 啟用/禁用指令跟蹤。 
traceMethodCalls(on) 啟用/禁用方法呼叫跟蹤。

AWT Desktop 容器單例

Desktop 類允許 Java 應用程式啟動已在本機桌面上註冊的關聯應用程式,以處理 URI 或檔案。支援的操作包括:

  • 開啟瀏覽器: 啟動使用者預設瀏覽器來顯示指定的 URI;
  • 開啟郵件客戶端: 啟動帶有可選 mailto URI 的使用者預設郵件客戶端;
  • 開啟檔案/資料夾: 啟動已註冊的應用程式,以開啟、編輯 或 列印 指定的檔案。

Desktop 通過一個容器來管理單例物件

public class Desktop {
    // synchronized 同步方法
    public static synchronized Desktop getDesktop(){
        if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
        if (!Desktop.isDesktopSupported()) {
            throw new UnsupportedOperationException("Desktop API is not " + "supported on the current platform");
        }
        sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
        Desktop desktop = (Desktop)context.get(Desktop.class); // 獲取單例物件
        // 存在則返回,不存在則建立,建立後put進容器
        if (desktop == null) {
            desktop = new Desktop(); 
            context.put(Desktop.class, desktop);
        }
        return desktop;
    }

AppContext 中有一個 HashMap 物件table,是實際的容器物件

private final Map<Object, Object> table = new HashMap();

spring AbstractFactoryBean

AbstractFactoryBean 類

public final T getObject() throws Exception {
    if (this.isSingleton()) {
        return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance();
    } else {
        return this.createInstance();
    }
}

private T getEarlySingletonInstance() throws Exception {
    Class<?>[] ifcs = this.getEarlySingletonInterfaces();
    if (ifcs == null) {
        throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references");
    } else {
        if (this.earlySingletonInstance == null) {
            // 通過代理建立物件
            this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler());
        }
        return this.earlySingletonInstance;
    }
}

Mybatis ErrorContext ThreadLocal

ErrorContext 類,通過 ThreadLocal 管理單例物件,一個執行緒一個ErrorContext物件,ThreadLocal可以保證執行緒安全

public class ErrorContext {
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
    private ErrorContext() {
    }
    
    public static ErrorContext instance() {
        ErrorContext context = LOCAL.get();
        if (context == null) {
          context = new ErrorContext();
          LOCAL.set(context);
        }
        return context;
    }
    //...
}

參考:
http://www.hollischuang.com/archives/197
https://www.cnblogs.com/chiclee/p/9097772.html
https://blog.csdn.net/abc123lzf/article/details/82318148

後記

歡迎評論、轉發、分享,您的支援是我最大的動力

更多內容可訪問我的個人部落格:http://laijianfeng.org

關注【小旋鋒】微信公眾號,及時接收博文推送

關注_小旋鋒_微信公眾號