設計模式——單例模式


單例模式是最簡單的也是設計模式系列書籍開篇第一個講到的模式,在平時的開發中也經常用它來保證獲取的都是同一個實例。

定義:確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

餓漢模式

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

    //限制外部產生HungrySingleton對象
    private HungrySingleton(){ }

    //向外提供獲取示例的靜態方法
    public static HungrySingleton getInstance() {
        return singleton;
    }

    //other methods
}

餓漢模式是類加載時候就創建對象,利用了jvm特性保證了線程的安全性。

  • getInstance()方法是static的保證了通過類名可直接獲取實例
  • 私有構造方法保證了只有自己才可以創建實例

懶漢模式

雙重檢查加鎖 方式

 1 public class LazySingleton {
 2     private static volatile LazySingleton singleton = null;
 3 
 4     private LazySingleton() { }
 5 
 6     public static LazySingleton getSingleton() {
 7         if (singleton == null) { //不用每次獲取對象都強制加鎖,為了提升了效率
 8             synchronized (LazySingleton.class) {
 9                 if (singleton == null) {
10                     singleton = new LazySingleton();
11                 }
12             }
13         }
14         return singleton;
15     }
16 }
  • 第7行代碼判空是為了提高效率,不用每次獲取對象都強制加鎖;
  • 第8行同步加鎖是為了線程安全;
  • 第9行判空是為了保證單例對象的唯一性,只有沒被創建才去創建。
  • volatile關鍵字為了保證singleton對象的可見性並禁止編譯器對其進行編譯指令重排序優化。

靜態內部類 模式

內部類有用static修飾和不用static修飾的內部類:

  • 用static修飾的是靜態內部類,相當於其外部類的static成員,不依賴與外部類的實例,靜態內部類只可以引用外部類的靜態成員屬性和靜態方法,只在第一次使用到靜態內部類的時候才會被裝載
  • 不用static修飾的內部類稱為對象級別內部類,依賴於外部的類實例
public class Singleton2 {
    /**
     * 類級的內部類,也就是靜態的成員式內部類,該內部類的實例與外部類的實例
     * 沒有綁定關系,而且只有被調用到時才會裝載,從而實現了延遲加載。
     */
    private static class Singleton2Holder {
        /**
         * 靜態初始化器,由JVM來保證線程安全
         */
        private static Singleton2 singleton = new Singleton2();
    }

    private Singleton2() {
        //System.out.println("singleton2 private construct method called");
    }

    public static Singleton2 getSingleton() {
        //System.out.println("singleton2 getSingleton method called");
        return Singleton2Holder.singleton;
    }

    private String name;

    public void desc() {
        //System.out.println("singleton2 desc method called");
    }
}

是餓漢模式的變種形式,利用jvm加載保證線程安全,並且實現了懶加載,只在獲取實例的時候才去創建。

JDK中單例的應用

Runtime單例實現

使用的是餓漢模式

破壞單例模式

反射破壞單例模式

/**餓漢模式——反射創建對象*/
Class<HungrySingleton> singletonClass = HungrySingleton.class;
Constructor<HungrySingleton> singletonConstructor = singletonClass.getDeclaredConstructor();
singletonConstructor.setAccessible(true);
/**先反射創建*/
HungrySingleton hungrySingleton = singletonConstructor.newInstance();
/**再通過單例模式獲取實例*/
HungrySingleton instance = HungrySingleton.getInstance();

System.out.println(hungrySingleton);
System.out.println(instance);

通過反射修改構造函數可以被訪問,通過反射構造的結果和單例模式獲取的不是同一個對象。

序列化破壞單例模式

HungrySingleton instance = HungrySingleton.getInstance();
jsonString = JSON.toJSONString(instance);
HungrySingleton singleton = JSON.parseObject(jsonString, HungrySingleton.class);
System.out.println(instance == singleton);

防止被破壞

防止反射創建對象

反射通過調用構造函數來創建對象,如果在構造函數里拋出異常,就可以組織反射創建對象(這種方式不適用與懶漢模式)。

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

    //限制外部產生Singleton對象
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("不允許創建對象!");
        }
        System.out.println("singleton private construct method called");
    }

    //向外提供獲取示例的靜態方法
    public static HungrySingleton getInstance() {
        System.out.println("create singleton instance");
        return singleton;
    }
}

再用反射創建對象會報錯

/**餓漢模式——反射創建對象*/
Class<HungrySingleton> singletonClass = HungrySingleton.class;
Constructor<HungrySingleton> singletonConstructor = singletonClass.getDeclaredConstructor();
singletonConstructor.setAccessible(true);
/**先反射創建*/
HungrySingleton hungrySingleton = singletonConstructor.newInstance();
/**再通過單例模式獲取實例*/
HungrySingleton instance = HungrySingleton.getInstance();

System.out.println(hungrySingleton);
System.out.println(instance);

懶漢模式仍然會被破壞(當反射先於懶漢模式創建對象時,仍然會創建多個對象)

/**懶漢模式——反射創建對象*/
Class<LazySingleton> lazySingletonClass = LazySingleton.class;
Constructor<LazySingleton> lazySingletonConstructor = lazySingletonClass.getDeclaredConstructor();
lazySingletonConstructor.setAccessible(true);
/**先通過反射獲取實例*/
LazySingleton lazySingleton = lazySingletonConstructor.newInstance();
/**再通過單例模式獲取實例*/
LazySingleton lazyInstance = LazySingleton.getInstance();

System.out.println(lazySingleton);
System.out.println(lazyInstance);

懶漢模式仍然會創建兩個對象:

singleton.LazySingleton@593634ad 

singleton.LazySingleton@20fa23c1

防止反序列化創建對象

如果是使用ObjectInputStream方式序列化,可以使用readResolve方法來控制。但序列化的方法有很多種,這種方式並不可靠。

枚舉單例

《effective java》第77條:對於實例控制,枚舉類型優於readResolve。

以下是一個枚舉單例的示例

public enum EnumSingleton {
    INSTANCE;

    public String getDesc() {
        return "desc";
    }

    public static void process() {
        System.out.println("static process method");
    }

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
}

使用時候直接獲取 EnumSingleton.INSTANCE 即可獲取當前實例,而且序列化與反序列化不會創建對象。

/**枚舉獲取實例*/
EnumSingleton instance = EnumSingleton.INSTANCE;
instance.setName("this is a enum singleton");
instance.setAge(28);
System.out.println("instance.name:"+instance.getName()+", instance.age:"+instance.getAge());

/**序列化*/
String jsonString = JSON.toJSONString(instance);
/**反序列化創建對象*/
EnumSingleton serializerInstance = JSON.parseObject(jsonString, EnumSingleton.class);
System.out.println(instance == serializerInstance);

Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
Constructor<EnumSingleton> enumSingletonConstructor = enumSingletonClass.getDeclaredConstructor();
enumSingletonConstructor.setAccessible(true);
/**反射創建*/
EnumSingleton enumSingleton = enumSingletonConstructor.newInstance();
System.out.println(enumSingleton);

輸出:

 instance.name:this is a enum singleton, instance.age:28

true 

java.lang.NoSuchMethodException: singleton.EnumSingleton.<init>()

  at java.lang.Class.getConstructor0(Class.java:3082)

總結,實現單例模式的唯一推薦方法,使用枚舉類來實現。

guava中的單例

 guava里有個Suppliers提供了memoize方法,用法如下:

Suppliers.memoize(new Supplier<Object>() {
            @Override
            public Object get() {
                return new Demo();
            }
        });

查看其實現源碼,將傳入的Suppliers作為代理傳給MemoizingSupperlizer,返回一個類型為MemoizingSupperlizer類型的Supperlier對象;如果不是MemoizingSupperlizer類型,創建一個MemoizingSupperlizer實例返回:

MemoizingSupperlizer內部get方法使用double-check方式實現了只執行一次創建對象方法

同樣的還有 ExpiringMemoizingSupplier 方法,支持過期時間只有再次調用get方法創建對象(可以用來實現緩存)

參考

代碼示例

《設計模式之禪》
https://www.cnblogs.com/shangxinfeng/p/6754345.html
https://www.cnblogs.com/ttylinux/p/6498822.html?utm_source=itdadao&utm_medium=referral
https://blog.csdn.net/hintcnuie/article/details/17968261
https://www.cnblogs.com/ldq2016/p/6627542.html


免責聲明!

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



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