天天看點

單例模式全總結-懶漢、餓漢、雙重校驗鎖、靜态内部類、枚舉類

  • 懶漢(線程不安全)

public class LazySingleton{
    private static LazySingleton lazySingleton = null;  
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        if(lazysingleton == null){
            lazysingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}
           
  • 餓漢(浪費記憶體,空間換時間)

public class Singleton {
    private static Singleton singleton=new Singleton();
    private Singleton(){}
    public static Singleton getInsatnce(){
        return singleton;
    }
}
           
  • 雙重校驗鎖-懶漢-線程安全-反射攻擊

public class Singleton{
    private volatile static Singleton uniqueInstance = null;  
    private Singleton(){}                              //private 防止外部可以new
    public static Singleton getUniqueInstance(){       //public static 保證外部可以調用方法 (在public後加sychronized可以,但影響效率)
        if(uniqueInstance == null){                    //某線程完成此步可能失去CPU執行權
            sychronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
           

是以在上面的懶漢式代碼中 singleton = new Singleton()這句,這并非是一個原子操作,事實上在JVM中這句話大概做了下面3件事情

            1.給singleton配置設定記憶體

            2.調用Singleton的構造函數來初始化成員變量

            3.将singleton對象指向配置設定的記憶體空間(執行到這一步,singleton才是非null的了)

            但是在JVM的及時編譯器中,存在指令重拍的優化,也就是說,第二步和第三步的順序是無法保證的而導緻程式出錯

為什麼會有指令重排序

        處理器為啥要重排序?

        因為一個彙編指令也會涉及到很多步驟,每個步驟可能會用到不同的寄存器,

        CPU使用了流水線技術,也就是說,CPU有多個功能單元(如擷取、解碼、運算和結果),一條指令也分為多個單元,

        那麼第一條指令執行還沒完畢,就可以執行第二條指令,前提是這兩條指令功能單元相同或類似,

        是以一般可以通過指令重排使得具有相似功能單元的指令接連執行來減少cpu流水線中斷的情況。

new一個對象有幾個步驟

            1.看class對象是否加載,如果沒有就先加載class對象,2.配置設定記憶體空間,初始化執行個體,3.調用構造函數,4.傳回位址給引用。

            而cpu為了優化程式,可能會進行指令重排序,打亂這3,4這幾個步驟,導緻執行個體記憶體還沒配置設定,就被使用了。

        再用個線程A和線程B舉例。

            線程A執行到new Singleton(),開始初始化執行個體對象,由于存在指令重排序,這次new操作,先把引用指派了,還沒有執行構造函數。      

            這時時間片結束了,切換到線程B執行,線程B調用new Singleton()方法,發現引用不等于null,就直接傳回引用位址了,然後線程B執行了一些操作,

            就可能導緻線程B使用了還沒有被初始化的變量。

synchronized:

      1、synchronized加在非靜态方法前和synchronized(this)都是鎖住了這個類的對象,如果多線程通路,對象不同,就鎖不住,對象固定是一個,就可鎖住。

      2、synchronized(類名.class)和加在靜态方法前,是鎖住了代碼塊,不管多線程通路的時候對象是不是同一個,類對象

         能縮小代碼段的範圍就盡量縮小,能在代碼段上加同步就不要再整個方法上加同步,縮小鎖的粒度。

 指令重排---2和3可能調換順序

        1. 配置設定空間

        2. 執行個體化對象

        3. 引用指向空間

  • 靜态内部類-線程安全-懶漢式-無法傳參

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
           

 使用靜态内部類能保證線程安全的原因

    1. 由于内部靜态類隻會被加載一次,故該實作方式是線程安全的

    2. 類加載的初始化階段是單線程的

靜态内部類的優點是:外部類加載時并不需要立即加載内部類,内部類不被加載則不去初始化INSTANCE,故而不占記憶體。

        即當SingleTon第一次被加載時,并不需要去加載SingleTonHoler,隻有當getInstance()方法第一次被調用時,才會去初始化INSTANCE,

        第一次調用getInstance()方法會導緻虛拟機加載SingleTonHoler類,這種方法不僅能確定線程安全,也能保證單例的唯一性,同時也延遲了單例的執行個體化。

  類加載時機:JAVA虛拟機在有且僅有的5種場景下會對類進行初始化。

        1.遇到new、getstatic、setstatic或者invokestatic這4個位元組碼指令時,

            為什麼外部類加載時靜态内部類未加載,《effective java》裡面說靜态内部類隻是剛好寫在了另一個類裡面,

            實際上和外部類沒什麼附屬關系。(但直接放在外部,1. 如果設定為public通路沒有限制 2. private的話通路受限)

            對應的java代碼場景為:

                new一個關鍵字或者一個執行個體化對象時、

                讀取或設定一個靜态字段時(final修飾、已在編譯期把結果放入常量池的除外)、

                調用一個類的靜态方法時。

        2.使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒進行初始化,

                需要先調用其初始化方法進行初始化。

        3.當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。

        4.當虛拟機啟動時,使用者需要指定一個要執行的主類(包含main()方法的類),虛拟機會先初始化這個類。

        5.當使用JDK 1.7等動态語言支援時,

            如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,

            并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。這5種情況被稱為是類的主動引用,注意,

            這裡《虛拟機規範》中使用的限定詞是"有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,

            稱為被動引用。靜态内部類就屬于被動引用的行列。

  我們再回頭看下getInstance()方法,調用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler裡的INSTANCE對象,

        跟上面那個DCL方法不同的是,getInstance()方法并沒有多次去new對象,故不管多少個線程去調用getInstance()方法,

        取的都是同一個INSTANCE對象,而不用去重新建立。

        當getInstance()方法被調用時,SingleTonHoler才在SingleTon的運作時常量池裡,

        把符号引用替換為直接引用,這時靜态對象INSTANCE也真正被建立,然後再被getInstance()方法傳回出去,這點同餓漢模式。

        那麼INSTANCE在建立過程中又是如何保證線程安全的呢?在《深入了解JAVA虛拟機》中,有這麼一句話:

      虛拟機會保證一個類的<clinit>()方法在多線程環境中被正确地加鎖、同步,如果多個線程同時去初始化一個類,

        那麼隻會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。

        如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個程序阻塞(需要注意的是,其他線程雖然會被阻塞,

        但如果執行<clinit>()方法後,其他線程喚醒之後不會再次進入<clinit>()方法。同一個加載器下,一個類型隻會初始化一次。),

        在實際應用中,這種阻塞往往是很隐蔽的。

    故而,可以看出INSTANCE在建立過程中是線程安全的,是以說靜态内部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的執行個體化。

    那麼,是不是可以說靜态内部類單例就是最完美的單例模式了呢?其實不然,靜态内部類也有着一個緻命的缺點,就是傳參的問題,

      由于是靜态内部類的形式去建立單例的,故外部無法傳遞參數進去,例如Context這種參數,是以,我們建立單例時,可以在靜态内部類與DCL模式裡自己斟酌。

  • 枚舉模式(單元素的枚舉類型已經成為實作Singleton的最佳方法)

public enum SingleTon{
    INSTANCE;
    public void method(){
        //TODO
    }
}

SingleTon.INSTANCE;
           

枚舉在java中與普通類一樣,都能擁有字段與方法,而且枚舉執行個體建立是線程安全的,在任何情況下,它都是一個單例。我們可直接以SingleTon.INSTANCE

的方式調用。

為什麼要用枚舉類實作單例

    雙重檢查鎖存在兩個問題

    1. 私有化構造器并不保險,反射攻擊

    2. 序列化問題