Android設計模式之——單例模式之原始碼使用場景(一)

1008711發表於2019-03-02

以下是五種單例模式,開發中都會用到

一、例項

/**
 * User:xijiufu
 * Time:2016/4/4,0:00
 * Function : 普通員工
 */
public class Staff {

    public void work() {
        //幹活
    }
}複製程式碼
/**
 * User:xijiufu
 * Time:2016/4/4,0:01
 * Function :副總裁
 */
public class VP extends Staff {
    @Override
    public void work() {
        //管理下面的人
    }
}複製程式碼
/**
 * User:xijiufu
 * Time:2016/4/4,14:11
 * Function : 惡漢模式
 */
public class CEO extends Staff {

    private static final CEO mCeo = new CEO();

    private CEO() {
    }

    public static CEO getCeo() {
        return mCeo;
    }

    @Override
    public void work() {
        //管理VP
    }
}複製程式碼
/**
 * User:xijiufu
 * Time:2016/4/4,14:12
 * Function :公司
 */
public class Company {

    private List<Staff> allStaffs = new ArrayList<>();

    public void addStaff(Staff per) {
        allStaffs.add(per);
    }

    public void showAllStaffs() {
        for (Staff per : allStaffs) {
            System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:" + per.toString());
        }
    }
}複製程式碼

測試結果:

public class SingletonTest {

    public static void main(String[] strings) {
        Company cp = new Company();
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        cp.addStaff(ceo1);
        cp.addStaff(ceo2);
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        Staff staff3 = new Staff();

        cp.addStaff(vp1);
        cp.addStaff(vp2);
        cp.addStaff(staff1);
        cp.addStaff(staff2);
        cp.addStaff(staff3);

        cp.showAllStaffs();

    }
    
}複製程式碼

輸出的結果是:

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:com.mrxi.detailed.tools.design_pattern.singleton.CEO@78308db1
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:com.mrxi.detailed.tools.design_pattern.singleton.CEO@78308db1
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:com.mrxi.detailed.tools.design_pattern.singleton.VP@27c170f0
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:com.mrxi.detailed.tools.design_pattern.singleton.VP@5451c3a8
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:com.mrxi.detailed.tools.design_pattern.singleton.Staff@2626b418
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:com.mrxi.detailed.tools.design_pattern.singleton.Staff@5a07e868
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Staff:com.mrxi.detailed.tools.design_pattern.singleton.Staff@76ed5528

從上述的程式碼中可以看出,CEO類不能通過new的形式構造物件,只能通過CEO.getCEO() 函式來 獲取,而這個CEO物件是靜態物件,並且在宣告的時候就已經初始化了,這就保證了CEO物件的唯一性。從輸出的結果可以看出,兩次CEO輸出的物件都是一樣的,VP與Staff物件是不同的。這個實現的核心在於將CEO類的構造方法私有化,使得外部程式不能通過建構函式來構造CEO物件,而CEO類通過一個靜態方法返回一個靜態物件。

二、單例模式

(1)、懶漢模式

懶漢模式是宣告一個靜態物件,並且在使用者第一次呼叫getInstance時進行初始化,而上述的餓漢模式是在宣告靜態物件時就已經初始化,懶漢模式實現如下:

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}複製程式碼

根據上面的程式碼發現getInstance方法中新增了synchronized關鍵字,也就是getInstance是一個同步方法,這就是上面所說的在多執行緒情況下保證單例物件唯一性的手段,細想幾十instance已經被初始化,第一次呼叫時就會被初始化instance,每次呼叫getInstance方法都會進行同步,這樣就消耗不必要的資源,這也是懶漢模式存在的問題。

懶漢模式的優點是單例只有在使用時才會被例項化,在一定程度上節約了資源,缺點是第一次載入時需要及時進行例項化,反應稍慢,最大的問題是每次呼叫getInstance都進行同步,造成不必要的同步開銷,所有不建議使用這種模式。

(2)、Double CheckLock (DCL) 實現單例

DCL方式實現單例模式的優點是既能夠在需要時才初始化單例,又能夠保證執行緒安全,且單例物件初始化後呼叫getInstance不進行同步鎖。

/**
 * User:xijiufu
 * Time:2016/4/4,14:37
 * Function :Double  CheckLock   DCL雙重檢查鎖定
 */
public class Singleton2 {

    private static Singleton2 sInstance = null;

    private Singleton2() {
    }

    public void doSomething() {
        System.out.println("so  sth.");
    }

    public static Singleton2 getInstance() {
        if (sInstance == null) {
            synchronized (Singleton2.class) {
                if (sInstance == null) {
                    sInstance = new Singleton2();
                }
            }
        }
        return sInstance;
    }

}複製程式碼

可以看到getInstance方法中對instance進行了兩次判空:第一層主要為了避免不必要的同步,第二層的判斷則是為了在null的情況下建立例項。

為什麼呢?

假設執行緒A執行到了instance=new Singleton2 ()語句,這裡看起來是一句程式碼,但實際上它並不是一個原子操作,什麼是原子操作?可以看這兒(baike.baidu.com/item/原子操作?f…),這句程式碼最終會被編譯成多條彙編指令,大致做了三件事:

①、給Singleton的例項分配記憶體;

②、呼叫Singleton()的建構函式,初始化成員欄位;

③、將instance物件指向分配的記憶體空間(此時instance就不是null);

DCL的優點:資源利用率高,第一次執行getInstance時單例物件才會被例項化,效率高。

缺點:第一次載入時反應稍慢,也由於Java記憶體模型的原因偶爾會失敗。在高併發環境下也有一定的缺陷,雖然發生的概率很小。DCL模式是使用最多的單例實現方式,它能夠在需要時才例項化單例物件,並且能夠在絕大多數場景下保證單例物件的唯一性,除非你的程式碼在併發場景比較複雜或者低於jdk1.6版本下使用,否則這種方式一般能夠滿足需求。

(3)、 靜態內部類單例模式

DCL雖然在一定程度上解決了資源消耗、多餘的同步、執行緒安全等問題,但是,它還是在某些情況下出現失效的問題。這個問題被稱為雙重檢查鎖定(DCL)失效,在《Java併發程式設計實踐》一書的最後談到這個問題,並指出這種“優化”是醜陋的,不贊成使用,建議使用如下程式碼:

/**
 * User:xijiufu
 * Time:2016/4/4,14:47
 * Function : 靜態內部類單例模式
 */
public class Singleton3 {
    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        return SingletonHolder.sInstance;
    }
    //靜態內部類
    private static class SingletonHolder {
        private static final Singleton3 sInstance = new Singleton3();
    }

}複製程式碼

當第一次載入Singleton類時並不會初始化sInstance,只有在第一次呼叫Singleton的getInstance方法時才會導致sInstance被初始化。因此,第一次呼叫getInstance方法會導致虛擬機器載入SingletonHolder類,這種方式不僅能夠確保執行緒安全,也能夠保證單例物件的唯一性,同時也延遲了單例的例項化,所以這是推薦使用的單例模式實現方式。

(4) 、列舉單例

前面說了三種單例模式實現,但是這些實現方式不是稍顯麻煩就是會在某些情況下出問題。還有沒有更簡單的實現方式呢?下面的程式碼:

/**
 * User:xijiufu
 * Time:2016/4/4,14:50
 * Function :
 */
public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {
        System.out.println(">>>>>>>>>>>>>>>>do.sth");
    }
}複製程式碼

列舉!

寫法簡單是列舉單例最大的優點,列舉在Java中與普通的類是一樣的,不僅能夠有欄位,還能夠有自己的方法。最重要的是預設列舉例項的建立是執行緒安全的,並且在任何情況下它都是一個單例。為什麼?在上述的幾種單例中,在一個情況下它們會出現重新建立物件的情況,那就是反序列化。

通過反序列化可以將一個單例的例項物件寫到磁碟,然後再讀回來,從而有效獲得一個例項。即使建構函式是私有的,反序列化時依然可以通過特殊的途徑去建立類的一個新的例項,相當於呼叫該類的建構函式。反序列化操作提供了一個很特別的鉤子函式,類中具有一個私有的、被例項化的方法readResolve,這個方法可以讓開發人員控制物件的反序列化。例如:上述幾個例項中如果要杜絕單例物件在被飯序列化重新生成物件,那麼必須加入如下方法:

private Object readResolve() throws ObjectStreamException {
    return sInstance;
}複製程式碼

也就是readResolve方法中將instance物件返回,而不是預設的重新生成一個新的物件。而對於列舉,並不存在這個問題,因為即使使用反序列化它也不會重新生成新的例項。

(5)、使用容器實現單例模式

通過以上的單例 模式之後 ,再來看看一種另類的實現,程式碼如下:

/**
 * User:xijiufu
 * Time:2016/4/4,14:55
 * Function :使用容器實現單例模式
 */
public class SingletonManager {

    private static Map<String, Object> objMap = new HashMap<>();

    private SingletonManager() {
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }

}複製程式碼

在程式的初始化時,將多種單例型別注入到一個統一的管理類中,在使用時根據key獲取物件對應型別的物件。這種方式使得我們可以管理多種型別的單例,並且在使用時也可以通過統一的介面進行獲取操作,降低了使用者的使用成本,也對使用者隱藏了具體實現,降低了耦合度。

不管以哪種形式實現單例模式,他們的核心原理都是講建構函式私有化,並且通過靜態方法獲取一個唯一的例項,在這個獲取的過程中必須保證執行緒安全、防止反序列化導致重新生成例項物件等問題。選擇哪種實現方式取決於專案本身,如是否是複雜的併發環境、jdk版本是否過低、單例物件的資源消耗等。

三、Android原始碼中的單例模式

在Android系統中,我們經常會通過Context獲取系統級別的服務,如WindowsManagerService、ActivityManagerService等,更常用的是一個LayoutInflater的類,這些服務會在合適的時候以單例的形式註冊在系統中,在我們需要的時候通過Context的getSystemService(String name)獲取。

相關文章