單例模式也能玩出花


一、單例模式

1、什么是單例模式

(1)單例模式

【單例模式(Singleton Pattern):】
定義:
    Ensure a class has only one instance, and provide a global point of access to it.
    直譯:確保一個類只有一個實例,並提供對它的全局訪問點(只允許通過全局訪問點獲取實例對象)。

 

(2)單例模式實現要點

一般情況下,訪問類中某變量、方法:
    可以通過 new 進行對象實例化,再通過 "對象名.變量名"、"對象名.方法名" 的形式獲取。
    可以通過 static 修飾(全局)變量、方法,再通過 "類名.變量名"、"類名.方法名" 的形式獲取。可避免使用 new 進行對象實例化。

為了保證一個類只存在一個實例,應該保證其有且只有 一次 實例化 機會:
    應該保證其構造方法不能在該類以外的地方被調用(防止使用 new 進行對象實例化)。
    構造方法只能在該類中被調用一次。
注:
    對象實例化常見方式: new、序列化、克隆、反射。
    
基本實現要點:
    構造方法私有化(防止使用 new 進行對象實例化)。
    在類的內部進行一次實例化(構造方法只能在該類中被調用一次)。
    對外提供一個全局訪問點(全局變量、全局方法等),可以通過 "類名.變量名" 或者 "類名.方法名" 的形式獲取實例對象(避免使用 new 進行對象實例化)。
注:
    反射會破壞 構造方法的私有化,需要注意,后面會介紹。
    序列化、克隆 等操作可能會破壞單例模式。需要注意。

 

(3)使用場景
  當頻繁創建、銷毀某個對象時,可以考慮單例模式。
  當創建對象消耗資源過多時,但又經常使用時,可以考慮單例模式。

 

2、常見單例模式實現方式

(1)實現方式

【餓漢式:】
    靜態變量
    靜態代碼塊
    枚舉(推薦)

【懶漢式:】
    靜態方法
    synchronized 同步方法
    synchronized 同步代碼塊
    雙重檢查
    靜態內部類

 

(2)餓漢式、懶漢式 區別

【基本區別:】
    懶漢式 在需要使用對象的時候才進行實例化操作。
    餓漢式 在類加載時完成實例化操作,可能暫時還不用該對象(占用內存)。

【餓漢式:】
核心:
    餓漢式借助 JVM 的類加載機制,在 類加載的初始化階段 完成 實例化操作。
    類初始化階段 只會執行一次,從而保證實例的唯一性 以及 線程安全。
    當類被主動使用時,才會導致類的初始化。而被動使用時,不會導致類的初始化。
主動使用類的方式:
    類的 main 方法被調用時。
    執行 new 實例化操作時。
    訪問靜態變量、靜態方法時。
    實例化子類時(先觸發父類初始化)。
    反射調用某類時。
JVM 類加載過程可參考: https://www.cnblogs.com/l-y-h/p/13496969.html#_label1_5

【懶漢式:】
核心:
    在需要使用對象的時候才進行實例化操作。
    多線程環境下,多個線程可能同時使用對象,需要考慮線程安全問題,防止並發訪問生成多個實例。

 

二、餓漢式

1、實現

(1)基本說明

【核心思路:】
    使用 static 關鍵字,借助類加載過程,進行實例的初始化。
    使用 private 修飾 構造方法,保證構造方法私有化。
    提供一個全局訪問點(類名.變量名 或者 類名.方法名)獲取對象。    
 
【可用方式:】
    靜態變量
    靜態方法
    靜態代碼塊

【優點:】
    在類加載的初始化階段完成了實例化,僅加載一次。保證對象的唯一性 以及 線程安全。
    
【缺點:】
    在類加載的初始化階段完成了實例化,沒有實現懶加載(Lazy Loading),可能造成內存的浪費(在不需要使用的時候被創建)。

 

(2)代碼實現(靜態變量)
  public 修飾變量,直接通過 "類名.變量名" 的方式獲取對象。

class HungrySingleton {
    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton singleton = new HungrySingleton();

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
    }
}

public class Test {
    public static void main(String[] args) {
        HungrySingleton singleton = HungrySingleton.singleton;
        HungrySingleton singleton2 = HungrySingleton.singleton;
        System.out.println(singleton == singleton2);  // true,為同一個對象
    }
}

 

(3)代碼實現(靜態方法)
  private 修飾變量,不允許通過 "類名.變量名" 的形式訪問。
  public 修飾方法,通過 "類名.方法名" 的方式獲取對象。

class HungrySingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton = new HungrySingleton();

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // true,為同一個對象
    }
}

 

(4)代碼實現(靜態代碼塊)
  靜態代碼塊,只是將實例化操作 移動到 靜態代碼塊中進行實現。

class HungrySingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行
    static {
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // true,為同一個對象
    }
}

 

(5)這就完了嗎?
  當然不是了,這樣寫只是防止了通過 new 實例化對象。
  對象實例化的方式還有 反射、序列化、克隆 等操作。
  這些操作是否會破壞單例模式?需要思考一下。

 

2、反射破壞

(1)類主動使用時,才會進行類的初始化
  類只有主動使用時,才會進行初始化操作。並不一定使用到類,就會觸發初始化操作。
比如:
  進行反射獲取私有構造方法時,並不會觸發 類加載過程。
  如下代碼執行后,靜態代碼塊中的 "start..." 不會輸出。

import java.lang.reflect.Constructor;

class HungrySingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行
    static {
        System.out.println("start...");
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor();
        hungrySingletonConstructor.setAccessible(true);
    }
}

 

(2)反射破壞
  如下代碼所示,反射調用構造方法時,會進行類加載過程(輸出 "start..." ),然后構建一個實例。
  此時的實例對象是通過 構造方法重新創建的對象。與類加載過程中創建的對象不同。
  即 反射對 單例模式造成了破壞。

import java.lang.reflect.Constructor;

class HungrySingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行
    static {
        System.out.println("start...");
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor();
        hungrySingletonConstructor.setAccessible(true); // 此時,還不會觸發 類加載過程
        HungrySingleton singleton = hungrySingletonConstructor.newInstance(); // 此時,觸發 類加載過程,並創建一個實例
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // false,不為同一個對象
     }
}

 

(3)防止反射破壞(未必會生效)
  在構造方法中,判斷實例是否已經被創建。
  類初始化過程中,會創建一個實例。即使通過反射調用構造方法,也會在實例創建之后再去調用,所以在 構造方法中進行判斷,實例存在則會拋出異常。從而防止反射破壞(未必會生效,后續序列化破壞中有提到)。

import java.lang.reflect.Constructor;

class HungrySingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    // 在 static 代碼塊中進行實例化,同樣在 類加載初始化階段 執行
    static {
        System.out.println("start...");
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("實例已存在,不允許重復創建");
        }
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor();
        hungrySingletonConstructor.setAccessible(true);
        HungrySingleton singleton = hungrySingletonConstructor.newInstance();
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // true,為同一個對象
    }
}

 

3、反序列化破壞

(1)序列化、反序列化
  序列化對象,並再次讀取對象時(反序列化),會創建一個新的對象。
注:
  序列化就是把實體對象狀態按照一定的格式寫入到有序字節流。
  反序列化就是從有序字節流重建對象,恢復對象狀態。

【反序列化核心代碼:】
ObjectInputStream 中的 readOrdinaryObject() 方法

private Object readOrdinaryObject(boolean unshared) throws IOException {
    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(desc.forClass().getName(), "unable to create instance").initCause(ex);
    }

    if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
        Object rep = desc.invokeReadResolve(obj);
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }
    return obj;
}

【關注點一:(調用構造函數實例化)】
desc.isInstantiable()
    如果一個 serializable/externalizable 的類可以在運行時被實例化,那么該方法就返回true。

desc.newInstance()
    通過反射調用無參構造創建一個對象。
注:
    此處調用的無參構造,與類本身的無參構造方法有差別。
    從實際效果上看,此處僅觸發了類加載,並未觸發類的構造函數。與前面提到的反射有區別。
    沒有深入研究,有興趣的可以幫忙解答一下。

【關注點二:(自定義對象生成策略)】
desc.hasReadResolveMethod()
    如果一個 serializable/externalizable 接口的類中包含 readResolve() 方法,則返回 true。

desc.invokeReadResolve(obj)
    通過反射的方式調用要被反序列化的類的 readResolve() 方法。

handles.setObject(passHandle, obj = rep)
    如果 readResolve() 返回的實例與構造方法創建的不同,則以 readResolve() 方法創建的實例為准。

 

(2)反序列化破壞
  如下代碼所示,通過反序列化創建了個對象。
  從實際代碼執行結果看,反序列化僅觸發了類加載過程(此時調用了構造函數),反序列化中 newInstance() 未主動觸發類的構造函數,所以此處構造方法中的判斷 無法防止 反序列化中反射的行為。
  即 反序列化對 單例模式造成了破壞。

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

class HungrySingleton implements Serializable {
    private static final long serialVersionUID = 42L;

    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("實例已存在,不允許重復創建");
        }
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
//        oos.writeObject(HungrySingleton.getInstance());

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        HungrySingleton singleton = (HungrySingleton) ois.readObject();

        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2); // false,不為同一對象
    }
}

 

(3)防止反序列化破壞
  通過 readResolve() 可以返回一個實例對象,保證此對象為類加載過程中創建的實例對象,即可防止 反序列化破壞。

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

class HungrySingleton implements Serializable {
    private static final long serialVersionUID = 42L;

    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("實例已存在,不允許重復創建");
        }
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }

    // 定義 readResolve() 方法,返回類加載過程中創建的實例對象(反序列化時返回此對象)
    private Object readResolve() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
//        oos.writeObject(HungrySingleton.getInstance());

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        HungrySingleton singleton = (HungrySingleton) ois.readObject();

        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2); // true,為同一對象
    }
}

 

4、克隆破壞

(1)克隆破壞
  如下代碼所示,通過克隆創建了個對象。
  調用了 Object 的 clone 方法(native 方法),與反序列化類似,也沒有觸發 類的構造方法(應該是直接從內存中 copy 了一份)。創建了一個新的對象。
  即 克隆對 單例模式造成了破壞。

class HungrySingleton implements Cloneable {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("實例已存在,不允許重復創建");
        }
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }

    // 重寫 clone() 方法,返回 clone 對象
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = (HungrySingleton) singleton.clone();
        System.out.println(singleton == singleton2); // false,不是同一對象
    }
}

 

(2)防止克隆破壞
  保證 clone() 方法返回的對象為類加載過程中創建的實例對象,即可防止 克隆破壞。

class HungrySingleton implements Cloneable {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 構造器私有化(防止通過new創建實例對象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("實例已存在,不允許重復創建");
        }
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static HungrySingleton getInstance() {
        return singleton;
    }

    // 重寫 clone() 方法,返回 clone 對象
    @Override
    public Object clone() throws CloneNotSupportedException {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = (HungrySingleton) singleton.clone();
        System.out.println(singleton == singleton2); // true,是同一對象
    }
}

 

5、枚舉

(1)基本說明

【基本說明:】
    寫個簡單的 enum 類,然后反編譯一下 javap -c xx.class。
    可以看到底層就類似於 餓漢式 靜態代碼塊 的寫法。在類加載的初始化階段完成實例化操作。

【優點:】
    在類加載的初始化階段完成了實例化,僅加載一次。保證對象的唯一性 以及 線程安全。
    可以防止 克隆、反序列化、反射 破壞單例模式。
    寫法簡單。

 

(2)反編譯一下 enum 類

【EnumSingleton】
enum EnumSingleton {
    INSTANCE;
}

【javap -c EnumSingleton.classfinal class pattern.sington.EnumSingleton extends java.lang.Enum<pattern.sington.EnumSingleton> {
  public static final pattern.sington.EnumSingleton INSTANCE;

  public static pattern.sington.EnumSingleton[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lpattern/sington/EnumSingleton;
       3: invokevirtual #2                  // Method "[Lpattern/sington/EnumSingleton;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lpattern/sington/EnumSingleton;"
       9: areturn

  public static pattern.sington.EnumSingleton valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class pattern/sington/EnumSingleton
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class pattern/sington/EnumSingleton
       9: areturn

  static {};
    Code:
       0: new           #4                  // class pattern/sington/EnumSingleton
       3: dup
       4: ldc           #7                  // String INSTANCE
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9                  // Field INSTANCE:Lpattern/sington/EnumSingleton;
      13: iconst_1
      14: anewarray     #4                  // class pattern/sington/EnumSingleton
      17: dup
      18: iconst_0
      19: getstatic     #9                  // Field INSTANCE:Lpattern/sington/EnumSingleton;
      22: aastore
      23: putstatic     #1                  // Field $VALUES:[Lpattern/sington/EnumSingleton;
      26: return
}

【等價於:】
public final class EnumSingleton extends Enum< EnumSingleton> {
    public static final EnumSingleton INSTANCE;
    public static EnumSingleton[] values();
    public static EnumSingleton valueOf(String s);
    static {
        INSTANCE = new EnumSingleton(name, ordinal);
    };
}

 

(3)防止反射破壞
  枚舉類型的類,沒有無參構造。默認繼承 Enum 的有參構造。

【代碼實現:】
import java.lang.reflect.Constructor;

enum EnumSingleton {
    INSTANCE;
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
        Constructor<EnumSingleton> enumSingletonConstructor = enumSingletonClass.getDeclaredConstructor(String.class, int.class);
        enumSingletonConstructor.setAccessible(true);
        // newInstance 會出現異常,java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        EnumSingleton enumSingleton = enumSingletonConstructor.newInstance();  

        EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE;

        System.out.println(enumSingleton == enumSingleton2);
    }
}

【原因分析:】
newInstance() 方法中進行判斷,若為枚舉類型,則拋異常。

@CallerSensitive
public T newInstance(Object ... initargs) throws IllegalArgumentException
{
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
}

 

(4)防止克隆破壞
  枚舉類型的類,無法重寫 clone() 方法。其父類 Enum 中定義 clone() 方法為 final 類型,不能被子類重寫。

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

 

(5)防止序列化破壞
  序列化返回的是同一個對象,無需定義 readResolve() 方法。其執行的是另一個邏輯。

【代碼實現:】
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

enum EnumSingleton {
    INSTANCE;
}

public class Test {
    public static void main(String[] args) throws Exception {
        EnumSingleton enumSingleton = EnumSingleton.INSTANCE;

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
        oos.writeObject(enumSingleton);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        EnumSingleton enumSingleton2 = (EnumSingleton) ois.readObject();

        System.out.println(enumSingleton == enumSingleton2); // true,是同一個對象
    }
}

【反序列化核心代碼:】
ObjectInputStream 中的 readEnum() 方法。
讀入並返回枚舉常量,如果枚舉類型不可解析,則返回 nullprivate Enum<?> readEnum(boolean unshared) throws IOException {

    ObjectStreamClass desc = readClassDesc(false);

    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    
    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

 

三、懶漢式

1、實現

(1)基本說明

【核心思路:】
    使用 private 修飾 構造方法,保證構造方法私有化。
    提供一個靜態的公共方法,在調用該方法時,才去創建實例對象。(全局訪問點,通過 "類名.方法名" 獲取對象)。    
 
【可用方式:】
    靜態方法
    synchronized 同步方法
    synchronized 同步代碼塊
    雙重檢查
    靜態內部類

【優點:】
    懶加載,需要使用對象時才會去實例化操作,提高內存利用率。

【缺點:】
    多線程環境下,多個線程可能同時使用對象,需要考慮線程安全問題,防止並發訪問生成多個實例。

 

(2)代碼實現(靜態方法)
  如下代碼所示,只允許通過 "類名.方法名" 的方式獲取對象。

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,通過 "類名.變量名" 訪問
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            fullSingleton = new FullSingleton();
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FullSingleton fullSingleton = FullSingleton.getInstance();
        FullSingleton fullSingleton2 = FullSingleton.getInstance();
        System.out.println(fullSingleton == fullSingleton2); // true,是同一個對象
    }
}

 

(3)這就完了嗎?
  當然不是了,這樣寫只是在單線程環境下正常執行。多線程操作下,會出現多個實例。
比如:
  線程 A 與線程 B 並發執行到 if (fullSingleton == null),此時兩個線程的 fullSingleton 均為 null,則均會進入方法,執行 new 實例化操作,此時便會產生多個實例對象。
  如下代碼所示,代碼執行多次可以發現,兩個線程輸出的對象並不一致。
  此時單例模式被破壞,線程不安全。

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            fullSingleton = new FullSingleton();
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@f3f9f4b
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@26af6bb1
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(4)代碼實現(synchronized 同步方法)
  為了保證線程安全,可以使用 synchronized 關鍵字實現同步。
注:
  synchronized 保證同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊。
  如下代碼所示,在方法上添加一個 synchronized,代碼執行多次可以發現,兩個線程輸出的對象始終一致。

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問
    public static synchronized FullSingleton getInstance() {
        if (fullSingleton == null) {
            fullSingleton = new FullSingleton();
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@40788638
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@40788638
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(5)這就完了嗎?
  當然不是了,雖然使用 synchronized 保證線程安全,但是這種方式鎖粒度太大,可能會導致執行效率低。

(6)代碼實現(synchronized 同步代碼塊)
  如下代碼所示,為了縮小 synchronized 影響范圍,可以在方法內部使用同步代碼塊的方式實現。

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            synchronized(FullSingleton.class) {
                fullSingleton = new FullSingleton();
            }
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@5eb39c2b
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@401a5cff
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(7)這就完了嗎?
  當然不是了,這樣寫又回到了 靜態方法中 提到的 線程不安全的問題上了。
比如:
  線程 A 與線程 B 並發執行到 if (fullSingleton == null),此時兩個線程的 fullSingleton 均為 null,則均會進入方法,遇到 synchronized,同步執行后,仍會執行 new 操作,產生多個實例對象。
  此時單例模式被破壞,線程不安全。雙重檢查可以解決這個問題。

 

2、雙重檢查

(1)代碼實現
  如下代碼所示,雙重檢查,在 synchronized 同步代碼塊 的基礎上,再添加一個判斷。

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            synchronized(FullSingleton.class) {
                if (fullSingleton == null) {
                    fullSingleton = new FullSingleton();
                }
            }
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance());
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance());
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(2)這就完了嗎?
  當然不是了。這樣寫看上去是保證了線程安全,但是有個細節需要思考一下(指令重排)。
  如下所示,反編譯一下代碼,可以看到實例化操作的相關指令。

【Test.java】
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
    }
}

【javap -c Test.classpublic class pattern.sington.Test {
  public pattern.sington.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class pattern/sington/Test
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: return
}

【關注 main 函數:】
    new 指令在 堆內存中為 Test 對象分配內存空間。
    invokespecial 指令,執行實例初始化操作。
    astore_1 指令,將棧頂引用類型值存入變量(即 使對象指向 堆內存空間)。
即分為三步:
    1、分配內存空間。
    2、實例初始化
    3、實例指向內存空間
注:
    按照常理說,1、2、3 是按照順序執行的。
    但是 JVM 會根據處理器特性,對指令進行優化(指令重排序),從而提高性能。
    指令重排,意味着指令可能不會按照指定順序執行。
    
【回到上例的 雙重檢查的代碼:】
    發生指令重排,new 實例化操作按照 1、3、2 的順序執行。
假設線程 A 執行完 1、3,但 2 還未執行完,即對象已指向內存空間,但是還沒有初始化。
此時線程 B 執行 getInstance() 代碼,由於對象已指向內存空間,判斷對象是否為 null 時返回 false, 跳過 synchronized 代碼塊。
此時線程 B 拿到的實例對象,由於初始化並未完成,使用對象將可能出現錯誤(引用逃逸)。
注: synchronized 並非原子性操作,可能發生指令重排。 使用 voliate 可以通過 內存屏障 禁止指令重排序。

 

(3)代碼實現(voliate )
  使用 voliate 修飾 變量,禁止指令重排序。

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    // volatile 防止指令重排
    private static volatile FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去檢查並創建一個實例對象。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            synchronized(FullSingleton.class) {
                if (fullSingleton == null) {
                    fullSingleton = new FullSingleton();
                }
            }
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@79af4c1c
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@79af4c1c
            }
        });

        thread.start();
        thread2.start();
    }
}

 

3、靜態內部類

(1)基本說明
  靜態內部類是一種結合了 餓漢模式、懶漢模式 優點的實現方式。

【核心思路:】
    使用 private 修飾 構造方法,保證構造方法私有化。
    在類的內部定義一個靜態內部類(只有被調用時,才會被加載),並在內部類中實例化對象。
    提供一個靜態的公共方法,在調用該方法時,調用靜態內部類。(全局訪問點,通過 "類名.方法名" 獲取對象)。    
 
【優點:】
    定義內部類,只有在用到的時候才回去加載,實現懶加載。
    使用 static 定義內部類,利用 JVM 類加載機制保證 線程安全。

 

(2)代碼實現
  如下代碼所示,定義一個靜態內部類。

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定義靜態內部類
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@2a75ae17
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@2a75ae17
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(3)這就完了嗎?
  當然不是了。反序列化破壞、反射破壞、克隆破壞 的問題同樣存在。
  解決方式與 餓漢模式的解決方式類似。

(4)防止序列化破壞
  重寫 readResolve() 方法,返回實例對象。

import java.io.*;

class FullSingleton implements Serializable {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定義靜態內部類
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }

    private Object readResolve() {
        return getInstance();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FullSingleton fullSingleton = FullSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
        oos.writeObject(fullSingleton);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        FullSingleton fullSingleton2 = (FullSingleton) ois.readObject();
        System.out.println(fullSingleton == fullSingleton2);
    }
}

 

(5)防止克隆破壞
  重寫 clone() 方法,返回實例對象。

class FullSingleton implements Cloneable {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
    }

    // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定義靜態內部類
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return getInstance();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FullSingleton fullSingleton = FullSingleton.getInstance();
        FullSingleton fullSingleton2 = (FullSingleton) fullSingleton.clone();
        System.out.println(fullSingleton == fullSingleton2);
    }
}

 

(6)防止反射破壞
  在構造方法中,新增一個判斷。

import java.lang.reflect.Constructor;

class FullSingleton {
    // 私有化變量,不可以通過 "類名.變量名" 的形式訪問
    private static FullSingleton fullSingleton;

    // 構造器私有化(防止通過new創建實例對象)
    private FullSingleton () {
        if (getInstance() != null) {
            throw new RuntimeException("實例已存在,創建失敗");
        }
    }

    // 提供一個全局訪問點,當調用該方法時,才去調用靜態內部類。通過 "類名.方法名" 訪問
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定義靜態內部類
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<FullSingleton> fullSingletonClass = FullSingleton.class;
        Constructor<FullSingleton> fullSingletonConstructor = fullSingletonClass.getDeclaredConstructor();
        fullSingletonConstructor.setAccessible(true);
        FullSingleton fullSingleton = fullSingletonConstructor.newInstance();

        FullSingleton fullSingleton2 = FullSingleton.getInstance();
        System.out.println(fullSingleton == fullSingleton2);
    }
}

 

四、舉例

1、JDK中的單例模式舉例(Runtime)

(1)部分源碼
  如下代碼所示,就是 餓漢模式的 實現。

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM