單例模式以及五種實現方式

目錄

 

什麼是單例

使用單例模式有什麼好處

應用場景

有哪幾種實現方式

其他的注意事項


什麼是單例

    單例模式根據名字可知就是一個類只能有一個單獨的實例並且對外提供一個訪問該實例的全局訪問點。

 

使用單例模式有什麼好處

單例模式只生成一個實例,減少了系統性能開銷。

當一個實例的創建需要較多資源時,比如讀取配置文件、產生其他依賴對象等,可以通過啓動應用時直接產生一個永駐內存的單例對象來避免重複創建對象產生的性能消耗。

 

應用場景

- 項目中讀取配置文件的類

- 數據庫連接池

- Spring中的Beans默認是單例

- Windows的任務管理器

等等~~

 

有哪幾種實現方式

單例模式的實現一共有以下五種方式,總的代碼步驟大多都分爲三步

a- 私有化構造器

b- 創建一個private static的本類實例

c- 提供一個public的公共方法對外提供實例

 

1-餓漢式

實現代碼如下

/**
 * 餓漢式單例
 * 線程安全,調用效率高但是存在資源浪費,不能延時加載
 */
public class SingletonDemo {

    //static對象在類加載時被創建,類加載的過程中是天然的線程安全的模式
    private static SingletonDemo instance = new SingletonDemo();
    private SingletonDemo(){

    }
    //instance已經在線程安全的狀態下創建過了,所以getInstance方法不需要加syncohronized關鍵字就是線程安全的.
    public static SingletonDemo getInstance(){
        return instance;
    }
}

優點:線程安全,調用效率高

缺點:存在資源浪費,不能延時加載

2-懶漢式

實現代碼如下

/**
 * 懶漢式延時加載
 * 線程安全,資源利用率高但getInstance被加鎖了,併發效率低.
 */
class SingletonDemo02{
    //類加載時不創建對象
    private static SingletonDemo02 instance ;
    private SingletonDemo02(){}
    //由於多線程調用getInstance時存在線程A和線程B同時執行到if(instance == null)部分,會導致兩個線程分別創建一個對象,違反單例的初衷,所以加鎖.
    public static synchronized SingletonDemo02 getInstance(){
        //當用到這個對象時getInstance方法被調用,纔會創建對象,避免了資源浪費.
        if (instance == null){
            instance = new SingletonDemo02();
        }
        return instance;
    }
}

優點:線程安全,資源利用率高,延時加載

缺點:併發效率低。懶漢式因爲getInstance加了鎖,是所有實現方式中效率最低的。

3-雙鎖檢測

實現代碼如下

/**
 * 雙重檢測鎖機制
 * 將synchronized塊放到了方法內
 */
class SingletonDemo03{
    private static SingletonDemo03 singleton;
    private SingletonDemo03(){}

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

雙鎖檢測機制是對懶漢式的一種改進,想要通過鎖定局部代碼不鎖方法的形式改良懶漢式併發訪問效率差的問題

理想很豐滿,但是因爲JVM內部結構的問題,該方法容易出錯,不能使用。

4-靜態內部類

實現代碼如下

/**
 * 靜態內部類方式
 * 線程安全,調用效率高,懶加載
 */
class SingletonDemo04{
    //main方法中測試了創建外部類對象時靜態內部類不會被加載
    public static void main(String[] args) {
        SingletonDemo04 s = new SingletonDemo04();
    }
    //靜態內部類在SingletonDemo04加載的時候不會同其他static對象一起加載,在用到的時候才加載
    public static class SingletonInstance{
        static {
            System.out.println("inner class loaded");
        }
        public static final SingletonDemo04 instance = new SingletonDemo04();
    }

    private SingletonDemo04(){}

    public static SingletonDemo04 getInstance(){
        return SingletonInstance.instance;
    }
}

靜態內部類的實現方式基本上集合了所有懶漢式和餓漢式的優點。最推薦使用的實現方式。

5-枚舉

/**
 * 通過枚舉創建單例對象
 */
enum SingletonDemo05{
    //本身就是單例對象
    INSTANCE;

    //用戶可添加其他操作
    public static void singletonOperation(){

    }
}

枚舉是通過JVM內部機制實現的,比較純天然。在效率上略低於靜態內部類的方式,也不能懶加載。沒有其他缺點。

 

其他的注意事項

單例的意義在於只能創建一個該類的實例,但是在實際使用時還是存在一些漏洞可以打破單例的限制(即便構造器是private的)。

1- 反射漏洞

反射通過Class.forName()是可以創建任何一個類的構造器的,而構造器又可以通過setAccessible(true)來訪問該類的private成員和方法,導致單例模式被打破。解決辦法如下:

    //若通過反射漏洞多次訪問構造方法,一定會在instance不爲null時另外創建其他對象,在構造方法中增加判斷條件即可.
    //注意,雖然構造方法是私有的,但是通過Constructor對象的setAccessible(true)是可以跳過私有檢查的.
    private SingletonDemo06(){
        if (instance != null){
            throw new RuntimeException("Instance is not null");
        }
    }

在構造器中設置一個instance是否爲空的檢測。

因爲單例模式是在instance爲空時創建對象,不爲空時直接返回現有的對象。也就是說構造器實際上只會在instance爲空時調用一次 。方法中的判斷條件可以有效防止反射漏洞。

2-反序列化漏洞

反序列化通過ObjectInputStream的readObject方式來加載被序列化的對象。(爲什麼readObject可以跳過private的權限檢查不確定,讀了幾行源碼感覺應該是先創建了ObjectInputStream對象然後強轉爲被序列化的類型,不能引導錯大家,此處還請大佬指點)

解決辦法

在被序列化的類中重寫readResolve()方法

    //定義了readResolve方法在反序列化過程中不會創建新的對象.
    public Object readResolve() throws ObjectStreamException{
        return instance;
    }

需要注意,反射和反序列化的漏洞都對枚舉的單例不起作用。因爲枚舉類型是絕對安全的。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章