1. 程式人生 > 其它 >【Monica的android學習之路】單例模式

【Monica的android學習之路】單例模式

技術標籤:android多執行緒java設計模式

【Monica的android學習之路】單例模式

1. 懶漢型

1.1 雙檢鎖+volatile

public class Singleton {
    private volatile static Singleton instance; //避免指令重排序問題

    private Singleton() {}

    public static Singleton getInstance
() { if (instance == null) { //第一層檢查 synchronized (Singleton.class) { if (instance == null) { //第二層檢查 instance=new Singleton(); } } } return instance; } }

為什麼必須配合volatile使用呢?

下面的程式碼在多執行緒環境下不是原子執行的。
instance=new DoubleCheckSingleton();

正常的底層執行順序會轉變成三步:
(1) 給DoubleCheckSingleton類的例項instance分配記憶體
(2) 呼叫例項instance的建構函式來初始化成員變數
(3) 將instance指向分配的記憶體地址

無論在A執行緒當前執行到那一步驟,對B執行緒來說可能看到A的狀態只能是兩種1,2看到的都是null,3看到的非null

執行緒A在重排序的情況下,上面的執行順序會變成1,3,2。現在假設A執行緒按1,3,2三個步驟順序執行,當執行到第二步的時候。B執行緒開始呼叫這個方法,那麼在第一個null的檢查的時候,就有可能看到這個例項不是null,然後直接返回這個例項開始使用,但其實是有問題的,因為物件還沒有初始化,狀態還處於不可用的狀態,故而會導致異常發生。

volatile如何發揮作用:
通過volatile關鍵詞來避免指令重排序,這裡相比可見性問題主要是為了避免重排序問題。如果使用了volatile修飾成員變數,那麼在變數賦值之後,會有一個記憶體屏障。也就說只有執行完1,2,3步操作後,讀取操作才能看到,讀操作不會被重排序到寫操作之前。這樣以來就解決了物件狀態不完整的問題。

lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:

1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,
也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對快取的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的快取行無效。

總結:
volatile的效果有:
原子性(不保證,與讀寫操作有關)
可見性(保證)
有序性(部分保證,涉及該變數的讀寫操作可以保證有序)

詳細介紹見《【Monica的android學習之路】同步關鍵字Synchronized和volatile》

1.2 靜態類載入

1.2.1 類載入順序

首先回顧一下類載入順序:

public class Main {
    private static String tag = "1";

    private String name = "2";

    static {
        System.out.println("this is static{}");
        tag += "3";
    }

    {
        System.out.println("this is {}");
    }

    Main() {
        System.out.println("this is Main()");
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("this is main()");
        System.out.println(tag);
        fun();
        System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
        Main instance = new Main();
        instance.fun2();
        System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
        Main instance2 = new Main();
        instance2.fun2();
    }

    private static void fun() {
        System.out.println("this is fun()");
        System.out.println("tag: " + tag);
    }

    private void fun2() {
        System.out.println("this is fun2()");
        name += "4";
        System.out.println("name: " + name);
    }
}

執行上述程式碼,輸出:
在這裡插入圖片描述

上面主要為了證明靜態方法在程式初始化時進行初始化,但是不會執行方法體
只有在呼叫時才執行方法體

總結:
程式開始執行時,分為兩個階段
(1)開始執行生命週期時,進入初始化階段。
在程式生命週期會且僅會執行一次,將依次初始化以下部分:
父類靜態變數
父類靜態程式碼塊
父類靜態方法(僅初始化不執行)
子類靜態變數
子類靜態程式碼塊
子類靜態方法(僅初始化不執行)

(2)初始化完成進入main函式,執行呼叫過程,當用到某個類,會首先進行類載入
該階段每進行一次new操作,會執行一遍如下過程,返回一個新的物件:
父類的非靜態變數
父類的非靜態程式碼塊
父類的非靜態方法(僅初始化不執行)
父類的構造方法
子類的非靜態變數
子類的非靜態程式碼塊
子類的非靜態方法(僅初始化不執行)
子類的構造方法

靜態方法和非靜態方法均只存在一份,無論有多少個物件存在,呼叫的方法是同一個。

1.2.2 類載入實現天然執行緒安全

public class BaseServiceBinder extends IBaseService.Stub {
    private BaseServiceBinder() {
    }

    private static class ServiceBinderLoader {
        private static final BaseServiceBinder binder = new BaseServiceBinder();   //靜態變數在初始化(程式啟動時)時就進行了初始化
    }

    public static BaseServiceBinder getInstance() { //靜態方法在呼叫時執行
        return ServiceBinderLoader.binder;
    }
}

2. 餓漢型

2.1 靜態變數

public class Singleton {
    private static Singleton instance = new Singleton(); //程式初始化時會例項化,僅執行一次

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {
    }
}

2.2 列舉

public enum EnumSingleton {
    SINGLETON;
}