Java單例的實現和分析

1.基本概念

目的: 保證類在內存中只有一個對象,可以直接訪問,不需要實例化該類的對象
注意:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
使用場景:
1、要求生產唯一序列號。
2、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來。
3、創建的一個對象需要消耗的資源過多,比如 I/O 與數據庫的連接等。
注意事項: getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化。

2.代碼實現

2.1.餓漢式(即時創建對象)

這種方式比較常用,但容易產生垃圾對象。空間時間
是否多線程安全:
實現難度:
優點: 沒有加鎖,執行效率會提高。
缺點: 類加載時就初始化,浪費內存。

public class Main {
    public static void main(String[] args) {
        //通過方法獲得唯一對象
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //判斷是否爲唯一對象
        System.out.println(s1 == s2);//運行結果爲true
    }
}
class Singleton {
    //1.私有構造方法,其他類不能訪問該構造方法
    private Singleton() {}
    //2.創建本類對象
    private static Singleton s = new Singleton();
    //3.對外提供公共的訪問方法
    public static Singleton getInstance() {//獲取實例
        return s;
    }
}

2.2.懶漢式(使用時才創建對象)

2.2.1.線程不安全的懶漢式

這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因爲沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式,不要求線程安全,在多線程不能正常工作。
是否多線程安全:
實現難度:

public class Main {
    public static void main(String[] args) {
    	//通過方法獲得唯一對象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
class SingletonLazy {
    //1.私有構造方法,其他類不能訪問該構造方法
    private SingletonLazy() {}
    //2.聲明一個引用
    private static SingletonLazy s;
    //3.對外提供公共的訪問方法
    public static SingletonLazy  getInstance() {//獲取實例
        //當沒有對象時,進行創建
        if(s==null){
            s = new SingletonLazy();
        }
        return s;
    }
}

2.2.2.線程安全的懶漢式

這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。
是否多線程安全:
實現難度:
優點: 第一次調用才初始化,避免內存浪費。
缺點: 必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。

public class Main {
    public static void main(String[] args) {
    	//通過方法獲得唯一對象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
class SingletonLazy {
    //1.私有構造方法,其他類不能訪問該構造方法
    private SingletonLazy() {}
    //2.聲明一個引用
    private static SingletonLazy s;
    //3.對外提供公共的訪問方法(使用synchronized鎖,防止線程搶佔)
    public static synchronized SingletonLazy  getInstance() {//獲取實例
        //當沒有對象時,進行創建
        if(s==null){
            s = new SingletonLazy();
        }
        return s;
    }
}

2.3.final實現方式(瞭解)

通過final關鍵字,實現對象在創建之後不可被更改。

public class Main {
    public static void main(String[] args) {
		//獲得唯一對象
        Singletonfinal s1 = Singletonfinal.s;
        Singletonfinal s2 = Singletonfinal.s;
        System.out.println(s1 == s2);//結果爲true
    }
}
class Singletonfinal{
    //1.私有構造方法,其他類不能訪問該構造方法
    private Singletonfinal() {}
    //2.聲明一個引用,final表示了地址不可被修改
    public static final Singletonfinal s = new Singletonfinal();
}

3.雙重校驗鎖(DCL,即double-checked locking)

3.1.DCL代碼實現

是否 Lazy 初始化:
是否多線程安全:
實現難度: 較複雜
描述: 這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。getInstance() 的性能對應用程序很關鍵。

public class Main {
    public static void main(String[] args) {
        SingletonDCL s1 = SingletonDCL.getInstance();
        SingletonDCL s2 = SingletonDCL.getInstance();
        System.out.println(s1 == s2);//結果是true
    }
}
class SingletonDCL {
    //1.構造私有方法
    private SingletonDCL() {}
    //2.懶漢式Lazy初始化,使用volatile可以保證可見性,也禁止指令重排序
    private volatile static SingletonDCL singletonDCL;
    //3.提供外部獲得實例的方法
    public static SingletonDCL getInstance() {
        //如果實例未創建,第一重
        if (singletonDCL == null)
            synchronized (SingletonDCL.class) {//通過字節碼,反射機制獲得對象,並上鎖
                //加上鎖後再次判斷,第二重
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        return singletonDCL;//返回對象
    }
}

3.2.使用volatile的原因(禁止指令重排序)

指令重排序: 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
例如:

 singletonDCL = new SingletonDCL();//實例化對象這行代碼

這行代碼實際上可以分解成三個步驟:
1.分配內存空間。
2.初始化對象。
3.將對象指向剛分配的內存空間。

但是有些編譯器因爲性能的原因,可能會改變2和3的順序,就成了:
1.分配內存空間。
2.將對象指向剛分配的內存空間。
3.初始化對象。

在不使用volatile且發生重排序的情況下,調用順序如下

線程一 線程二
檢查到singletonDCL爲null -
獲取鎖 -
再次檢查到singletonDCL爲null -
爲singletonDCL分配內存空間 -
將singletonDCL指向內存空間 -
- 檢查到singletonDCL不爲空
- 訪問singletonDCL(此時線程一還未初始化完成對象)
初始化singletonDCL -

在這種情況下,在線程二訪問singletonDCL時,訪問的是一個初始化未完成的對象。

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