深度認識單例模式;在Android源碼中的應用


深度認識單例模式;在Android源碼中的應用


前言

身為程序員,你可能沒有系統的學習過設計模式,但是你一定知道單例模式,因為它相對簡單,而且最常被大家所用到。既然大家都用到過,也都知道為什麼我還要單獨列出一篇文章來寫呢?

因為絕大部分開發者平時對單例模式的認識,可能僅僅停留在“會用”的階段。為什麼會有這個模式?為什麼要用這個模式?在哪裡用單例模式最合適?亂用了會有什麼負面影響?

這些可能大多數人都一知半解。今天就讓我們大家一起來扒光單例模式的外衣,有深度的認識一下單例模式。

通過這篇文章你能學到什麼

(建議你可以帶著問題去學習)

  1. 單例模式的定義
  2. 單例模式在Android源碼中的應用
  3. 單例模式的九種寫法以及優劣對比
  4. 單例模式的使用場景
  5. 單例模式存在的缺點
  6. 接下來我們就一起進入今天的學習了

單例模式的定義

在學單例模式之前,我想大家都會自己問自己:“單例模式存在的意義是什麼?我們為什麼要用單例模式?”

眾所周知,在古代封建社會,一個國家都只有一個國王或者叫皇帝。我們在這個國家的任何一個地方,只要提起國王,大家都知道他是誰。因為國王是唯一的。其實這個就是單例模式的核心思想:保證對象的唯一性。

單例模式(Singleton Pattern):確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。 單例模式是一種對象創建型模式。

從其定義我們可以看出來單例模式存在三個要點:

1、實例唯一性
2、自行創建
3、全局訪問

如何設計一個優秀的單例模式其實也是圍繞著這三點來的。

說了這麼多了,還不知道單例模式到底啥樣呢?接下來我們一起來著手設計這個“國王”的單例類。我們先看一下單例模式的類圖:

深度認識單例模式;在Android源碼中的應用

單例模式的類圖看起來很簡單,一個私有的當前類型的成員變量,一個私有的構造方法,一個 getInstance 方法,創建對象不再通過new 而通過 getInstance 讓該類自行創建。相信我們大多數人使用的單例模式都是這種,因為太簡單了。但是單例模式的寫法可不止這一種。接下來我們一起來看一下單例模式的九種寫法。

單例模式的九種寫法

一、餓漢式(靜態常量)

<code>/**
* 餓漢式(靜態常量)
*/
class King {
private static final King kingInstance = new King();

static King getInstance() {
return kingInstance;
}

private King() {
}
}
/<code>
  • 優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
  • 缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。

二、餓漢式(靜態代碼塊)

<code>/**
* 餓漢式(靜態代碼塊)
*/
class King {
private static King kingInstance;

static {
kingInstance = new King();
}

private King() {
}

public static King getKingInstance() {
return kingInstance;
}
}
/<code>
  • 優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
  • 缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。

三、懶漢式(線程不安全)

<code>/**
* 懶漢式(線程不安全)

*/
public class King {
private static King kingInstance;

private King() {
}

public static King getKingInstance() {
if (kingInstance == null) {
kingInstance = new King();
}
return kingInstance;
}
}
/<code>

優點:懶加載,只有使用的時候才會加載。

缺點:但是隻能在單線程下使用。如果在多線程下,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。所以在多線程環境下不可使用這種方式。

四、懶漢式(線程安全)

<code>/**
* 懶漢式(線程安全,同步方法)
*/
public class King {
private static King kingInstance;

private King() {
}

public static synchronized King getKingInstance() {
if (kingInstance == null) {
kingInstance = new King();
}

return kingInstance;
}
}
/<code>
  • 優點:懶加載,只有使用的時候才會加載,獲取單例方法加了同步鎖,保障線程安全。
  • 缺點:效率太低了,每個線程在想獲得類的實例時候,執行getInstance()方法都要進行同步。

五、懶漢式(線程安全,同步代碼塊)

<code>/**
* 懶漢式(線程安全,同步代碼塊)
*/
public class King {
private static King kingInstance;

private King() {
}

public static King getKingInstance() {
if (kingInstance == null) {
synchronized (King.class) {
kingInstance = new King();
}
}
return kingInstance;
}
}
/<code>
  • 優點:改進了第四種效率低的問題。
  • 缺點:不能完全保證單例,假如一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。

六、雙重檢查(DCL)

<code>/**
* 雙重檢查(DCL)
*/
public class King {

private static volatile King kingInstance;

private King() {
}

public static King getKingInstance() {
if (kingInstance == null) {
synchronized (King.class) {
if (kingInstance == null){
kingInstance = new King();
}
}
}
return kingInstance;
}
}
/<code>
  • 優點:線程安全;延遲加載;效率較高。
  • 缺點:JDK < 1.5 的時候不可用
  • 不可用原因:由於volatile關鍵字會屏蔽Java虛擬機所做的一些代碼優化,可能會導致系統運行效率降低,而JDK 1.5 以及之後的版本都修復了這個問題。(面試裝逼用,謹記!!!)

七、靜態內部類

<code>/**
* 靜態內部類
*/
public class King {

private King() {
}

private static class KingInstance{
private static final King KINGINSTANCE = new King();
}

public static King getInstance(){
return KingInstance.KINGINSTANCE;
}
}
/<code>
  • 優點:避免了線程不安全,延遲加載,效率高。
  • 缺點:暫無,最推薦使用。
  • 特點:這種方式跟餓漢式方式採用的機制類似,但又有不同。
  • 兩者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不同的地方在餓漢式方式是隻要Singleton類被裝載就會實例化,沒有Lazy-Loading的作用,而靜態內部類方式在Singleton類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的實例化。 類的靜態屬性只會在第一次加載類的時候初始化,所以在這裡,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。

八、枚舉

<code>/**
* 枚舉
*/
public enum King {
KINGINSTANCE;
}
/<code>
  • 優點:不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。
  • 缺點:JDK 1.5之後才能使用。

九、容器類管理

<code>/**
* 使用容器實現單例模式(可以用於管理單例,有興趣的可以嘗試一下)
* */
class InstanceManager {
private static Map<string> objectMap = new HashMap<>();
private InstanceManager(){}
public static void registerService(String key,Object instance){
if (!objectMap.containsKey(key)){
objectMap.put(key,instance);
}
}
public static Object getService(String key){
return objectMap.get(key);
}
}
/<string>/<code>
<code>/** 

* 使用方式
* Dog類就不貼出來了
* 自己隨便寫個就行
* 可以運行一下看看 打印的地址是否一致
*/
class Test {
public static void main(String[] args) {

InstanceManager .registerService("dog", new Dog());

Dog dog = (Dog) InstanceManager .getService("dog");
Dog dog2 = (Dog) InstanceManager .getService("dog");
Dog dog3 = (Dog) InstanceManager .getService("dog");
Dog dog4 = (Dog) InstanceManager .getService("dog");

System.out.println(dog);
System.out.println(dog2);
System.out.println(dog3);
System.out.println(dog4);
}
}
/<code>
  • 優點:在程序的初始,將多種單例類型注入到一個統一的管理類中,在使用時根據key獲取對象對應類型的對象。這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作, 降低了用戶的使用成本,也對用戶隱藏了具體實現,降低了耦合度。
  • 缺點:不常用,有些麻煩

九種寫法的優劣對比;

深度認識單例模式;在Android源碼中的應用

①:不可用原因:由於volatile關鍵字會屏蔽Java虛擬機所做的一些代碼優化,可能會導致系統運行效率降低,而JDK 1.5 以及之後的版本都修復了這個問題。(面試裝逼用,謹記!!!)

②:這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不同的地方在餓漢式方式是隻要Singleton類被裝載就會實例化,沒有Lazy-Loading的作用, 而靜態內部類方式在Singleton類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的實例化。 類的靜態屬性只會在第一次加載類的時候初始化,所以在這裡,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。

單例模式在Android源碼中的應用

在我們每天接觸的Android源碼中其實也有很多地方用到了單例模式:

1、EventBus中獲取實例:

<code>private static volatile EventBus defaultInstance;
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {

defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
/<code>

可以看到,EventBus採用的是雙重檢查(DCL)的方式實現的單例模式。

2、InputMethodManager獲取實例

<code>static InputMethodManager sInstance;
public static InputMethodManager getInstance() {
synchronized (InputMethodManager.class) {
if (sInstance == null) {
IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
sInstance = new InputMethodManager(service, Looper.getMainLooper());
}
return sInstance;
}
}
/<code>

我們看到,其實這裡是懶漢式(同步代碼塊)方式的改寫,去掉了外部判斷為空,放到了裡面。然後通過ServiceManger.getService()方法,通過容器的方式獲取了單例。

在Android很多系統服務都是通過容器獲取的單例。

單例模式在日常開發中的應用場景

日常開發中我們也有些場景是需要用到單例模式的,例如:

1、圖片加載
2、網絡請求
3、工具類封裝

案例有很多,相信大家也都有用到,我就不列舉了。這裡我們如何合理的在項目中使用單例模式。

合理的辨析一個設計是否應該為單例模式前,大家先問問自己幾個問題,也是檢驗標準:

Quote from 《 Use your singletons wisely 》

Will every application use this class exactly the same way? (keyword: exactly)Will every application ever need only one instance of this class? (keyword: ever & one)Should the clients of this class be unaware of the application they are part of?

每一個應用(組件/模塊)是否以完全一致的方式來使用這個類?
每一個應用(組件/模塊)是否真的只需要這個類的一個實例呢?
對於這個類的客戶端類來說,對他們自己是應用中的一部分這件事是否應該保持毫無察覺的狀態呢?

以上3條就是檢驗一個類是否應該被設計為單例模式的判斷準則,

如果我們對於以上這3條均給出了“是的”的答案,那麼這個類就是可以被設計為單例模式了。反之還是不要用的好。

單例模式的優點

單例模式的優點其實已經在定義中提現了:可以減少系統內存開支,減少系統性能開銷,避免對資源的多重佔用、同時操作。

單例模式的缺點

任何事物都不是完美的,單例模式也是如此,它也存在以下幾個缺點:

1、違反了單一責任鏈原則,測試困難

單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的創建和產品的本身的功能融合到一起。

2、擴展困難

由於單例模式中沒有抽象層,因此單例類的擴展有很大的困難。修改功能必須修改源碼。

3、共享資源有可能不一致。

現在很多面向對象語言(如Java、C#)的運行環境都提供了自動垃圾回收的技術,因此,如果實例化的共享對象長時間不被利用,系統會認為它是垃圾,會自動銷燬並回收資源,下次利用時又將重新實例化,這將導致共享的單例對象狀態的丟失。

總結

今天我們通過文章學習了第一個設計模式,瞭解了他的設計理念,學會了他的九種寫法,也認識了他的優缺點。相信大家已經對單例模式有了一個全新的認識。(反正我寫完文章才認識到自己原來根本不瞭解單例模式)

最後還是要給大家說一句話:模式是死的,代碼是活的。不要硬套模式。代碼會告訴你怎麼做,你聽就是了。(也是借鑑前輩們的經驗)


分享到:


相關文章: