單例的8種實現

單例模式是比較簡單的設計模式,但是涉及到的知識點還是挺多,比如併發模式下的單例、序列化反序列化情況下保證單例、反射情況下保證單例,下面來看看各種情況下怎麼保證單例。

    單例模式最基本的組成構造函數私有化、即不能隨便創建對象;對外提供一個靜態方法來獲取對象。

 一、餓漢模式

1、餓漢模式實現

public class Single {
    /**
     * 類成員變量
     */
    private static Single single = new Single();
    private Single() {
        System.out.println("私有構造函數");
    }
    public static Single getInstance() {
        return single;
    }
}

 

2、餓漢模式單例對象創建流程 

(1)、invokestatic 指令觸發類加載、初始化。

(2)、類加載流程:類加載(Class對象生成)、連接(驗證.class 文件、準備、解析)、初始化。

(3)、類成員變量在準備階段被分配內存空間並賦予初始值,分配的空間是在方法區。

(4)、初始化階段是執行類構造器<clinit> 方法的過程。

(5)、<clinit>類構造器方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併產生的。編譯器收集的順序是由語句在原文中出現的順序決定的。

(6)、類成員變量初始化:定義變量直接賦值、靜態代碼塊賦值,不管是那種賦值方式都是使用 <cinit>類構造器中putstatic 指令來給類成員變量賦值。

(7)、虛擬機會保證一個類的<cinit> 方法在多線程環境中被正確的加鎖、同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<cinit>方法,其他線程都要阻塞等待,直到活動線程執行<clinit>方法完畢。

(8)、一個線程執行<clinit> 類構造方法,其他線程雖然會被阻塞,但如果執行<clinit> 方法的那條線程退出<clinit> 方法後,其他線程喚醒之後不會在進入<clinit> 方法。同一個類加載器下,一個類型只會初始化一次。

(9)、類變量初始化結束

3、線程安全問題

    餓漢模式在<clinit> 類構造器中主動創建對象,按照上面的創建流程發現、餓漢單例模式是線程安全的。  

二、懶漢模式

1、懶漢模式實現

public class Single {
    /**
     * 類成員變量
     */
    private static Single single ;
    private Single() {
        System.out.println("私有構造函數");
    }
    public static Single getInstance() {
        if (null == single) {
            single = new Single();
        }
        return single;
    }
}

 

2、單例對象創建時間點

    他這個懶漢模式懶在哪裏,當調用invokestatic 指令之後進行加載類、連接、初始化;對象的創建不是在類初始化<clinit>方法中而是延遲到了靜態方法中……

3、懶漢模式線程安全問題

if(null == single){
    Single  single= new Single() 
}

 

4、多線程環境下執行存在線程安全問題

ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 30; i++) {
    service.execute(new Runnable() {
        @Override
        public void run() {
            Single single = getInstance();
            System.out.println(single);
        }
    });
}

 

5、執行結果

私有構造函數私有構造函數com.sb.design.single.Single@2deeefc5私有構造函數com.sb.design.single.Single@35655ed5com.sb.design.single.Single@2e1b63f0com.sb.design.single.Single@35655ed5com.sb.design.single.Single@2e1b63f0

    多次調用構造函數創建對象而且對象地址也不相同……

    唯一性判斷在單線程環境沒問題,但是在多線程併發執行情況下這個判斷條件不能保證原子性,也不能保證複合原子性,所以這樣的寫法在多線程環境下無法保證單例。

三、靜態內部類(懶漢+餓漢)

1、靜態內部類實現單例模式

public class Single implements Serializable {
    static {
        System.out.println("1、外部類加載、鏈接、初始化");
    }
    private Single() {
        System.out.println("4、調用私有構造函數初始化單例對象");
    }
    /**
     * 獲取枚舉類對象
     *
     * @return
     */
    private static class InnerSingle {
        static {
            System.out.println("3、靜態內部類加載、鏈接、初始化");
       }
        private static final Single SINGLE = new Single();
    }

    /**
     * 對外發布單例對象
     *
     * @return
     */
    public static Single getInstance() {
        System.out.println("2、外部類加載、鏈接、初始化完成");
        return InnerSingle.SINGLE;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Single single = Single.getInstance();
        Single single1 = Single.getInstance();
        System.out.println(single == single1);
    }
}

 

2、靜態內部類執行流程

1、外部類加載、鏈接、初始化2、外部類加載、鏈接、初始化完成3、靜態內部類加載、鏈接、初始化4、調用私有構造函數初始化單例對象2、外部類加載、鏈接、初始化完成true

3、爲何說內部類單例是懶漢+餓漢模式我們分析一下靜態內部類的加載順序

(1)、當有getstatic、setstatic、invokestatic 等指令調用外部類則會觸發外部類的加載、鏈接、初始化過程,但是在這過程不會觸發靜態內部類的加載(靜態內部類、非靜態內部類的加載和外部類的加載無關),這裏我們調用了外部類靜態方法即觸發了invokestatic 指令,所以進行了外部類的加載、鏈接、初始化流程的執行。(2)、當外部類初始化結束後開始執行外部類靜態方法(3)、外部類靜態方法中調用了內部類的靜態常量,這樣內部類又觸發了getstatic 指令(4)、內部類觸發getstatic指令開始執行內部類的類加載、連接、初始化流程(5)、內部類初始化就是內部類的類構造器執行過程即<clinit> 方法執行(6)、按照開始分析的餓漢模式加載流程知道clinit方法中有putstatic 指令即給內部類的靜態變量賦值(7)、putstatic 指令賦值的具體對象就是調用外部類的私有構造函數創建的單例對象(8)、結束了靜態內部類單例的生成流程,按照<clinit> 方法執行是線程安全的來看,靜態內部類也是線程安全的。

4、多線程測試

public static void main(String[] args) throws IOException, ClassNotFoundException {
    ExecutorService executor = Executors.newFixedThreadPool(50);
    for (int i = 0; i < 100; i++) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Single single = Single.getInstance();
                Single single1 = Single.getInstance();
                System.out.println(single == single1);
            }
        });
    }
    executor.shutdown();
}

 

5、多線程測試執行結果

1、外部類加載、鏈接、初始化2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成3、靜態內部類加載、鏈接、初始化4、調用私有構造函數初始化單例對象2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成true2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成true2、外部類加載、鏈接、初始化完成2、外部類加載、鏈接、初始化完成true……

    只要看外部類私有構造函數調用次數就能證明線程安全,其實從上面的輸出也證明了<clinit> 類構造器方法的執行是線程安全的。

四、懶漢模式-多線程環境加類鎖

1、懶漢模式線程安全實現

public static Single getInstance() {
    synchronized (Single.class) {
        if (null == single) {
            single = new Single();
        }
        return single;
    }
}
    使用synchronized鎖類,這樣寫感覺沒問題,但是所有線程都需要獲得類鎖才能判斷是不是對象已創建,這樣這個鎖競爭很激烈,性能也就更低。

五、懶漢模式-多線程環境雙重鎖判斷

1、代碼實現

public static Single getInstance() {
    if (null == single) {
        synchronized (Single.class) {
            if (null == single) {
                single = new Single();
            }
        }
    }
    return single;
}
    雙重鎖校驗相比直接添加類鎖,鎖競爭沒那麼激烈,有性能提升但是,這種寫法又帶來了一個嚴重的問題即指令重排。

更嚴格的說這種寫法不是線程安全的,分析原因如下:

2、對象創建流程

1、通過new 指令判斷所指向的類是否已加載,如果未加載則進行加載。2、給對象在堆內存分配空間即指定一塊對內存給新建對象。3、對象初始化即<init> 對象構造函數執行。4、將新建對象地址賦值給引用變量。    但是在編譯和運行時會對現有代碼進行優化即指令重排序,排序之後對象創建順序有可能是1-2-3-4 也有可能是1-2-4-3 當出現 1-2-4-3 這種順序的時候上上面的雙重鎖校驗就有問題!

3、指令重排後的對象創建流程

    如果指令重排後是1-2-4-3 這樣的順序,當線程A執行到4即將對象內存地址賦值給引用變量,cpu資源被B線程佔用,這時線程B判斷不爲空則直接返回single對象,但是這個對象沒有被初始化;所以這樣的寫法在1-2-4-3 這樣的執行順序下是有問題的,而引起這個問題的根本原因是指令重排。

六、懶漢模式-指令重排

volatile 關鍵字是輕量級鎖,他能保證變量的可見性和防止指令重排(內存屏障)​​​​​​​

private static volatile Single single;
synchronized (Single.class) {
    if (null == single) {
        single = new Single();
    }
}
一個volatile關鍵字搞定;這樣多線程環境下的單例模式就沒問題了,可以安全使用!

七、使用序列化、反序列化破壞單例模式

1、序列化、反序列化破壞單例​​​​​​​

public class Single implements Serializable {
    /**
     * 類成員變量
     */
    private static volatile Single single;
    private Single() {
        System.out.println("私有構造函數");
    }
    /**
     * 雙重鎖校驗
     *
     * @return
     */
    public static Single getInstance() {
        if (null == single) {
            synchronized (Single.class) {
                if (null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException, NoSuchMethodException, IOException, ClassNotFoundException {
        Single single = getInstance();
        // 序列化
        String path = System.getProperty("user.dir");
        File file = new File(path + File.separator + "singles.text");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(single);
        // 反序列化
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Single single1 = (Single) objectInputStream.readObject();
        System.out.println(single == single1);
    }
}
    其實看序列化、發序列化的過程你會發現也是調用的反射來創建對象;使用序列化破壞單利很簡單,那怎麼防止序列化、反序列化破壞單利模式?

2、一步步分析反序列化過程、瞭解是怎麼破壞的單例

(1)、從輸入流獲取對象

(2)、readObject0獲取對象

(3)、獲取類序列化描述符類,並使用反射獲取反序列化新對象

(4)、使用constructor 構造函數對象創建新對象

(5)、readResolve方法判斷,如果序列化類沒有readResolve 方法則直接返回構造函數創建的對象,如果存在則獲取readResolve 方法返回值

(6)、反射調用readResolve 方法

(7)、用readResolve 方法返回值替換使用constructor 構造函數器對象創建的新對象

    從上面流程看到,如果序列化類無readResolve 方法則直接返回通過constructor 構造函數類對象創建的新對象,如果readResolve 方法存在則返回該方法返回值,所以在單例類序列化時添加該方法,可以避免序列化、反序列化破壞單例!

八、懶漢單例模式-反射破壞單利

 因爲返利能獲取類中的任何變量並且能重新設置值,所以不管是添加一個計數器還是判斷變量是否爲空都是無法避免反射破壞單例的;下面來看看使用計數器和判斷是否爲null來阻止破壞單例模式是否有效。​​​​​​​

public class Single implements Serializable {
    private static volatile boolean flag = true;
    private static volatile Single single;
    /**
     * 類成員變量
     */
    private Single() throws BindException {
        if (!flag) {
            throw new BindException("多次創建對象");
        }
        flag = false;
    }

    /**
     * @return
     * @throws BindException
     */

    public static Single getSingle() throws BindException {
        if (null == single) {
            synchronized (Single.class) {
                if (null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, BindException {

        Single single = Single.getSingle();
        Single single1 = Single.getSingle();
        System.out.println(single == single1);
        //反射設置屬性
        Class<Single> singleClass = Single.class;
        Field field = singleClass.getDeclaredField("flag");
        field.setAccessible(true);
        //設置屬性,滿足創建對象條件
        field.set(single, true);
        Constructor<Single> constructor = singleClass.getDeclaredConstructor(null);
        Single single2 = constructor.newInstance(null);
        System.out.println(single == single2);
    }
}

 

九、枚舉類解決序列化、反序列化以及反射等的相關問題

1、代碼實現​​​​​​​

public class Single implements Serializable {
    /**
     * 類成員變量
     */
    private Single() {
        System.out.println("私有構造函數");
    }
    /**
     * 內部類
     */
    private enum InnerClass implements Serializable {
        MY_SINGLE;
        private final Single single;
        /**
         * 枚舉類無慘構造函數
         */
        InnerClass() {
            System.out.println("內部類構造函數");
            single = new Single();
        }
        /**
         * 私有方法
         *
         * @return
         */

        private Single getSingle() {
            return single;
        }
    }

    /**
     * 獲取枚舉類對象
     *
     * @return
     */

    public static InnerClass getEnum() {
        return InnerClass.MY_SINGLE;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        InnerClass innerClass = getEnum();
        Single single = innerClass.getSingle();
        String path = System.getProperty("user.dir");
        File file = new File(path + File.separator + "single.txt");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(innerClass);
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        InnerClass innerClass1 = (InnerClass) objectInputStream.readObject();
        System.out.println(innerClass == innerClass1);
        Single single2 = innerClass1.getSingle();
        System.out.println(single == single2);
    }
}

 

2、執行結果​​​​​​​

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