天天看點

【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

文章目錄

  • 前言
  • 一.單例模式
  • 二.單例的使用場景
  • 三.單例模式優點
  • 四.單例的實作方式
    • 1.餓漢式
    • 2.懶漢式
    • 3.雙重校驗鎖式
    • 4.靜态内部類式
    • 5.枚舉單例式
    • 6.ThreadLocal式單例(單線程單例)
  • 五.單例模式漏洞
    • 1.反射破壞單例
    • 2.淺克隆破壞單例模式
    • 3.序列化破壞單例
  • 六.Spring容器式單例拓展
  • 七.總結

前言

建議先看這篇文章【設計模式】軟體設計7大原則以及23種設計模式初識(一)

一.單例模式

【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

單例模式的核心是保證一個類隻有一個執行個體,并且提供一個全局通路方法通路該執行個體。

  • 解決問題:避免一個全局使用的類頻繁地建立與銷毀
  • 如何解決:判斷是否已經有這個單例,如果有則傳回,如果沒有則建立。
  • 關鍵代碼:
    1.将類的構造函數設為私有。 類的靜态方法仍能調用構造函數, 但是其他對象不能調用。
    2.在類中添加一個私有靜态成員變量用于儲存單例執行個體。
    3.聲明一個公有靜态方法用于擷取單例執行個體。
    4.在靜态方法中實作"延遲初始化"。 該方法會在首次被調用時建立一個新對象, 并将其存儲在靜态成員變量中。 此後該方法每次被調用時都傳回該執行個體。
               

二.單例的使用場景

  1. Spring中bean對象的模式實作方式
  2. servlet中每個servlet的執行個體
  3. spring mvc和struts1架構中,控制器對象是單例模式
  4. MyBatis的SqlSessionFactory對象、,Spring的BeanFactory對象都是單例的
  5. 讀取配置檔案的類,一般也隻有一個對象。沒有必要每次使用配置檔案資料,每次new一個對象去讀取。
  6. 資料庫連接配接池的設計一般也是采用單例模式,因為資料庫連接配接是一種資料庫資源

三.單例模式優點

優點:

  1. 在記憶體裡隻有一個執行個體,減少了記憶體的開銷,尤其是頻繁的建立和銷毀執行個體
  2. 避免對資源的多重占用(比如寫檔案操作)。

缺點:沒有接口,不能繼承,與單一職責原則沖突,一個類應該隻關心内部邏輯,而不關心外面怎麼樣來執行個體化。

四.單例的實作方式

實作方式 優缺點
餓漢式 線程安全,調用效率高 ,但是不能延遲加載
懶漢式 線程安全,調用效率不高,能延遲加載
雙檢鎖/雙重校驗鎖式(DCL,即 double-checked locking) 由于JVM底層内部模型原因,偶爾會出問題。不建議使用
登記式/靜态内部類式 線程安全,資源使用率高,可以延時加載
枚舉單例式 線程安全,調用效率高,但是不能延遲加載

1.餓漢式

就是

類加載的時候立即執行個體化對象

,實作的步驟是

先私有化構造方法,對外提供唯一的靜态入口方法

,實作如下

public class SingletonInstance1 {
    // 聲明此類型的變量,并執行個體化,當該類被加載的時候就完成了執行個體化并儲存在了記憶體中
    private static SingletonInstance1 singletonInstance = new SingletonInstance1();

    // 私有化所有的構造方法,防止直接通過new關鍵字執行個體化
    private SingletonInstance1() {
    }

    // 對外提供一個擷取執行個體的靜态方法
    public static SingletonInstance1 getInstance() {
        return singletonInstance;
    }
}
           

餓漢式通過

classloader 機制

保證在類加載(

static變量會在類裝載時初始化

)的時候就立即初始化對象,此時也不會涉及多個線程對象通路該對象的問題。

虛拟機保證隻會裝載一次該類,肯定不會發生并發通路的問題

。是以,可以省略

synchronized

關鍵字

  • 優點: 沒有加鎖,執行效率高,性能比懶漢式更好
  • 缺點: 類加載的時候就初始化,不管用與不用都會占用空間,浪費記憶體。

2.懶漢式

public class SingletonInstance2 {
    // 聲明此類型的變量,但沒有執行個體化
    private static SingletonInstance2 singletonInstance = null;

    // 私有化所有的構造方法,防止直接通過new關鍵字執行個體化
    private SingletonInstance2() {

    }

    // 對外提供一個擷取執行個體的靜态方法,為了資料安全添加synchronized關鍵字
    public static synchronized SingletonInstance2 getInstance() {
        return singletonInstance;
    }
}
           

外部調用 getInstance()

的時候才會執行個體化對象。進而實作了

延遲加載

,但因為在方法上添加了

synchronized關鍵字

上鎖,每次調用getInstance方法都會

同步

,是以對

性能

的影響比較大

3.雙重校驗鎖式

public class SingletionInstance3 {
    // 聲明此類型的變量,,但沒有執行個體化(使用volatile關鍵字修飾會直接操作主記憶體)
    private volatile static SingletionInstance3 singletionInstance = null;

    // 私有化所有的構造方法,防止直接通過new關鍵字執行個體化
    private SingletionInstance3() {

    }

    // 對外提供一個擷取執行個體的靜态方法,
    public static SingletionInstance3 getInstance() {
        if (singletionInstance == null) {
            synchronized (SingletionInstance3.class) {
                if (singletionInstance == null) {
                    singletionInstance = new SingletionInstance3();
                }
            }
        }
        return singletionInstance;
    }
}
           

4.靜态内部類式

public class SingletonInstance4 {
    //使用SingletonInstance4 的時候,預設先初始化内部類SingletonClassInstance 
	//如果不使用,則内部類不加載
    private static class SingletonClassInstance {
        // 聲明外部類型的靜态常量
        public static final SingletonInstance4 SINGLETON_INSTANCE = new SingletonInstance4();
    }
    
    // 私有化構造方法
    private SingletonInstance4() {
    }

    // 對外提供的唯一擷取執行個體的方法
    public static SingletonInstance4 getInstance() {
        return SingletonClassInstance.SINGLETON_INSTANCE;
    }
}
           

注意點:

  1. 外部類沒有

    static

    屬性,則不會像餓漢式那樣立即加載對象。
  2. 隻有真正調用

    getInstance()

    ,才會加載靜态内部類。加載類時是線程 安全的,

    SINGLETON_INSTANCE 是static final類型

    ,保證了

    記憶體中隻有這樣一個執行個體存在,而且隻能被指派一次

    ,進而保證了線程安全性.
  3. 兼備了并發高效調用和延遲加載的優勢!

5.枚舉單例式

public enum  SingletonInstance5 {
    // 定義一個枚舉元素,則這個元素就代表了SingletonInstance5的執行個體
    INSTANCE;

    public void singletonOperation(){
        // 功能處理
    }

    public static void main(String[] args) {
        SingletonInstance5 s1  = SingletonInstance5.INSTANCE;
        SingletonInstance5 s2  = SingletonInstance5.INSTANCE;
        System.out.println(s1 == s2); // 輸出的是 true
    }
}
           

枚舉類會在類加載的時候會初始化裡面的所有的執行個體,而且 JVM 保證了它們不會再被執行個體化,是以它天生就是單例的。

優點:

  1. 實作簡單
  2. 枚舉本身就是單例模式

    。由JVM從根本上提供保障!

    避免通過反射和反序列化的漏洞

缺點:

  1. 無延遲加載

6.ThreadLocal式單例(單線程單例)

ThreadLocal不能保證其建立的對象是

全局唯一

,但是能保證

在單個線程中是唯一的,天生的線程安全

public class ThreadLocalSingleton {
	//初始化執行個體
    private static final ThreadLocal<ThreadLocalSingleton> singleton =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

 	// 對外提供的唯一擷取執行個體的方法
    public static ThreadLocalSingleton getInstance(){
        return singleton.get();
    }

	// 私有化構造方法
    private ThreadLocalSingleton() {}
}
           

五.單例模式漏洞

1.反射破壞單例

通過反射的方式我們依然可用擷取多個執行個體(

除了枚舉的方式

)

public static void main(String[] args) throws Exception, IllegalAccessException {
        //擷取餓漢式單例對象
        SingletonInstance1 s1a = SingletonInstance1.getInstance();
        SingletonInstance1 s1b = SingletonInstance1.getInstance();
        System.out.println(s1a+"----"+s1b);



        // 反射方式擷取執行個體
        Class<SingletonInstance1> c1 = SingletonInstance1.class;
        Constructor<SingletonInstance1> constructor = c1.getDeclaredConstructor(null);
        //放開私有構造器通路權限
        constructor.setAccessible(true);
        //調用構造方法執行個體化對象
        SingletonInstance1 s2 = constructor.newInstance(null);
        System.out.println(s2);
    }
           

執行結果

【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

結論:通過調用getInstance()擷取的對象是同一個對象,但是序列化單例類會重新一個新的對象,不符合單例模式思想了。

  • 解決方式:在

    無參構造方法中手動抛出異常控制

// 私有化所有的構造方法,防止直接通過new關鍵字執行個體化
    private SingletonInstance1() {
        if(singletonInstance != null){
            // 隻能有一個執行個體存在,如果再次調用該構造方法就抛出異常,防止反射方式執行個體化
            throw new RuntimeException("單例模式隻能建立一個對象");
        }
    }
           

再次執行main方法

【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

2.淺克隆破壞單例模式

public class SingletonInstanceClone implements Cloneable{
    // 聲明此類型的變量,并執行個體化,當該類被加載的時候就完成了執行個體化并儲存在了記憶體中
    private static SingletonInstanceClone singletonInstance = new SingletonInstanceClone();

    // 私有化所有的構造方法,防止直接通過new關鍵字執行個體化
    private SingletonInstanceClone() {
        if (singletonInstance != null) {
            // 隻能有一個執行個體存在,如果再次調用該構造方法就抛出異常,防止反射方式執行個體化
            throw new RuntimeException("單例模式隻能建立一個對象");
        }
    }

    // 對外提供一個擷取執行個體的靜态方法
    public static SingletonInstanceClone getInstance() {
        return singletonInstance;
    }

    //淺克隆破壞單例
    public static void main(String[] args) throws Exception, IllegalAccessException {
        SingletonInstanceClone s1a = SingletonInstanceClone.getInstance();
        SingletonInstanceClone s1b = (SingletonInstanceClone) s1a.clone();
        System.out.println(s1a);
        System.out.println(s1b);
    }
}
           

測試結果

【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

解決方式:重寫clone()然後傳回唯一執行個體

@Override
    protected Object clone() throws CloneNotSupportedException {
        return singletonInstance;
    }
           
【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

3.序列化破壞單例

通過反序列化的方式也可以破解上面幾種方式 (

除了枚舉的方式

)

一個單例對象建立後,有時候需要将對象序列化然後寫入磁盤,下次使用的時候再從磁盤中讀取對象并進行反序列化,将其轉化為對象。反序列化後的對象會重新配置設定記憶體,即重新建立對象。如果序列化的目标對象為單例對象,就違背了單例模式的初衷,相當于破壞了單例
  • 序列化

    就是把記憶體中的對象的狀态通過轉換成位元組碼的形式存儲到磁盤進而轉換一個I/O流,寫入其他地方;記憶體中的狀态資料會永久的儲存下來。
  • 反序列化

    就是将已經持久化的位元組碼内容轉換成I/O流,通過I/O流讀取,進而将讀取的内容轉換成java對象
  • 了解序列化和反序列化可以點選看這篇文章,寫的清楚
  • 注意: 序列化SingletonInstance1類要實作 Serializable 接口,否則抛出異常java.io.NotSerializableException

public static void main(String[] args) throws Exception, IllegalAccessException {
        SingletonInstance1 s1 = SingletonInstance1.getInstance();

        // 将執行個體對象序列化到檔案中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));//目前項目所在的目前會生成一個a.text
        oos.writeObject(s1);
        oos.flush();
        oos.close();


        // 将執行個體從檔案中反序列化出來
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
        SingletonInstance1 s2 = (SingletonInstance1) ois.readObject();
        ois.close();

        System.out.println(s1);
        System.out.println(s2);
    }
           

執行結果

【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

結論:序列化後會生成一個新的對象,同樣破壞了單例模式

  • 解決辦法: 在單例類中重寫

    readResolve方法

    并在該方法中傳回單例對象即可
/**
 * 餓漢式
 */
public class SingletonInstance1 implements Serializable {
    // 聲明此類型的變量,并執行個體化,當該類被加載的時候就完成了執行個體化并儲存在了記憶體中
    private static SingletonInstance1 singletonInstance = new SingletonInstance1();

    // 私有化所有的構造方法,防止直接通過new關鍵字執行個體化
    private SingletonInstance1() {
        if (singletonInstance != null) {
            // 隻能有一個執行個體存在,如果再次調用該構造方法就抛出異常,防止反射方式執行個體化
            throw new RuntimeException("單例模式隻能建立一個對象");
        }
    }

    // 對外提供一個擷取執行個體的靜态方法
    public static SingletonInstance1 getInstance() {
        return singletonInstance;
    }

    // 重寫該方法,防止序列化和反序列化擷取執行個體
    private Object readResolve() throws ObjectStreamException {
        return singletonInstance;
    }
}
           

再次執行main()

【設計模式】建立型模式—單例模式(Singleton Pattern)(二)前言一.單例模式二.單例的使用場景三.單例模式優點四.單例的實作方式五.單例模式漏洞六.Spring容器式單例拓展七.總結

結論: 序列化後擷取的是同一個對象,說明

readResolve方法是基于回調的,反序列化時,如果定義了readResolve()則直接傳回此方法指定的對象,而不需要在建立新的對象!

六.Spring容器式單例拓展

核心思想是: 建立一個容器,把所有的對象都放到容器中,在存儲對象的時候:如果容器中已經存在了就不放,如果容器中不存在對象則放到容器。

public class SingletonContainer {

    //建立一個線程安全的map對象
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    //構造器私有
    private SingletonContainer() {
    }

    //存儲單例對象
    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();//反射建立對象
                    ioc.put(className, obj);//放到容器
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            }

            //如果存在這個對象
            return ioc.get(className);

        }
    }
}
           
public static void main(String[] args) {
		//注意存放的是公有的構造方法
		Object obj1 = SingletonContainer.getBean("com.hanker.entity.Student");
		Object obj2 = SingletonContainer.getBean("com.hanker.entity.Student");
		System.out.println(obj1);
		System.out.println(obj2);
		System.out.println(obj1 == obj2);
	}
           

容器式單例模式适用于

執行個體非常多的情況

,便于管理。但是它是

非線程安全的

Spring的ApplicationContext實作的容器就是單例模式的

七.總結

  1. 一般情況下,不建議使用懶漢方式,建議使用餓漢方式。
  2. 隻有在要明确實作延遲加載時,才會使用第 靜态内部方式。
  3. 如果涉及到反序列化建立對象時,可以嘗試使用第枚舉方式。
  4. 如果有其他特殊的需求,可以考慮使用雙重校驗鎖方式。

繼續閱讀