java 常見單例使用和相關的內存泄露問題

這幾天剛好在外面面試,被問到設計模式,心血來潮就想記下這方面的東西。像單例設計模式我覺得從大家開始學習的時候就已經寫了對應的代碼,但是到了面試的時候還是拎不清。

記憶中的單例

學習設計模式方面的知識知道單例的目的,“爲了讓類的一個對象成爲系統中的唯一實例,需要用一種只允許生成對象類的唯一實例的機制。‘阻止’所有想要生成對象的訪問”

那麼單例的三要素就是

  1. 某個類只能有一個實例
  2. 它必須自行創建這個實例
  3. 它必須自行向整個系統提供這個實例

從代碼角度上講,也就是

  • 構造方法私有
  • 含有一個該類的靜態私有對象
  • 提供了一個靜態的公有的函數用於獲取該類靜態私有對象

加上後面學了UML,便有了這麼一張圖

 

記得畢業出來找工作一被問到,馬上就說單例實現有常見兩種形式,懶漢式跟餓漢式,對,是餓漢,不是惡漢,之前看過別人的面試題就寫惡漢,嚇死人了要。但是還是從面試官的嘴角看到一抹邪魅的上揚,但是又不知道發生了什麼,還以爲面試官欣慰地一笑,後面才發現我的內心戲有點多。

這裏還是繼續舉出最原始的兩個模板代碼

懶漢式

public class Singleton {
    private Singleton(){
    }
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

餓漢式 

public class Singleton {
    private Singleton(){
    }
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}
public class Singleton {
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    private Singleton() {}
    public Singleton getInstance() {
        return instance;
    }
}

對比

  對象聲明 實例化時機 線程安全 性能
懶漢式 類私有對象一直爲空 方法被調用時候 無線程同步處理機制 達到Lazy Loading的效果,但未處理同步
餓漢式 類私有對象定義時初始化 類初始化階段 類裝載的時候就完成實例化。避免了線程同步問題 沒有達到Lazy Loading的效果,如果沒有調用就造成資源浪費

那我覺得如果是這樣的話,我在懶漢式那邊加個同步不就可以了麼,於是就有了下面的代碼,美滋滋的單例完成了。雙重檢查,一層性能二層安全,振振有詞。

public class Singleton {
    private Singleton(){
    }
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

但是我一跑,會發現在某種偶然情況下,我執行了但是他居然告訴我空指針異常?可代碼這樣沒問題啊,引用私有持有,構造方法私有,最後也提供鎖的方式去獲取單例。 這裏我們就需要引入一個這樣的修飾符“volatile”,查了網上很多的說法,都在解釋一個“可見性”,麻煩大家自行搜索“volatile+可見性”,具體不表。不管怎樣,咱們先記住,添加該修飾符的目的就是爲了防止指令重排序。畢竟new出對象並賦值屬於非原子操作,所以這樣做就會偶現上面所說的空指針問題。一個volatile搞定,這就是一個完整的雙重檢查。


雙重檢查

public class Singleton {
    private Singleton(){
    }
    private static volatile Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

靜態內部類和代碼塊實現

public class Singleton {
    // 私有構造
    private Singleton() {}

    // 靜態內部類
    private static class InnerProvider{
        private static Singleton single = new Singleton();
    }
    
    public static Singleton getInstance() {
        return InnerProvider.single;
    }
}

靜態內部類形式的單例可以保證單例在多線程併發下的線程安全性,但是在遇到序列化對象時,默認的方式運行得到的結果就是多例的

public class Singleton {
    
    // 私有構造
    private Singleton() {}
    
    private static Singleton single = null;

    // 靜態代碼塊
    static{
        single = new Singleton();
    }
    
    public static Singleton getInstance() {
        return single;
    }
}

 靜態代碼塊形式的單例藉助java的類初始化機制進行對象實例,同理衍生出下面的容器型單例

容器型單例

還有一種,就是創建用容器來存儲。像安卓裏面的應用啓動流程裏面,對各種SystemService的初始化也是如此。我們可以看到,其初始化就是把這些服務的實例都存儲在了一個map中,獲取服務對象的時候,先判斷map中對應key能否拿到有效的系統服務對象,如果有,直接get獲取。沒有的話就創建完,再put到map中去。這也是一種單例。爲什麼?

因爲這個map是靜態的,那麼也就是類加載階段,完成該容器的初始化,那麼這就代表這個容器只會被初始化一次。那麼對應該容器的集合元素,都是隻有一個,故這樣實現的單例。

枚舉單例

上面說了這些,但是我每次要兩次去檢查,我也很煩,所以這裏我們可以用到枚舉的單例。爲什麼枚舉適合單例?

首先枚舉的對象在jvm中會是唯一內存,jvm對枚舉對象已經有了自己的一套機制,默認枚舉實例的創建是線程安全的。

其次,枚舉單例能自動避免序列化/反序列化攻擊,因爲枚舉類不能通過反射生成。所以枚舉單例也是《Effective Java》中推薦的模式。

但是枚舉存在一個性能問題,尤其是Android的應用場景中,枚舉需要佔用較大的內存,如果對內存敏感,請儘量少使用,換用做靜態常量,所以Android中儘量減少枚舉的使用。

public enum SingleEnum {
    SINGLE_ENUM("str",666);


    private String mString;
    private int mInt;

    SingleEnum(String mString, int mInt) {
        this.mString = mString;
        this.mInt = mInt;
    }

    public  String getStirng(){
        return mString;
    }

    public int getInt(){
        return mInt;
    }
}

 

內存泄露場景

在介紹完單例以後,我們來說說內存泄露的問題。從GC角度上看,不再會被使用的對象的內存不能被回收。而單例上容易出現的內存泄露就是長生命週期的對象持有短生命週期的引用。來看下面這段代碼


public final class Utils {
    public Activity mContext;
    public InitParam mParam;
    public void init(final Activity context, InitParam param) {
        mContext = context;
        mInitParam = param;
    }
    public void doSomething(){...}
}

是不是覺得有點熟悉?但這個是我出來工作後接手別人項目時候的常見代碼塊,像工具類也好,自定義控件也好,都會存在一個需要上下文的場景,但是很多時候的做法是直接持有所在上下文的引用,這已經是在危險邊緣試探了。如上述代碼所示,當銷燬該界面的時候,準備回收的時候,又發現工具類裏還持有着當前界面的引用,那GC就忍不住吐槽“這tm叫我怎麼回收,不收了!”於是乎,你出現了內存泄露還沾沾自喜地在下個界面使用着這個工具類,甚至揚言,沒事,反正已經初始化過了。

或者我們可以嘗試這樣,不直接持有上下文,只在使用的時候傳入上下文,或者傳入回調對象去操作上下文即可。如下


public final class Utils {
    // public Activity mContext;
    public InitParam mParam;
    public void init(InitParam param) {
        // mContext = context;
        mInitParam = param;
    }
    public void doSomething(Activity context){...}

    public void doSomething(ActivityCallback context){...}

}
public interface ActivityCallback{
        public void callback();
    }

public TargetActivity implement ActivityCallback{...}

避免直接持有,但是假設我是個槓精,我就要持有,怎麼辦!可以,滿足你!

public final class Utils {
    /**
     * 弱引用形式持有activity
     */
    private WeakReference<Activity> mWeakReferInitActivity;

    public InitParam mParam;

    public void init(final Activity activity, InitParam param) {
        mWeakReferInitActivity = new WeakReference<>(activity);
        mInitParam = param;
    }
    public void doSomething(){...}

    /**
     * @return 返回上下文對象
     */
    public Activity getActivity() {
        return mWeakReferInitActivity.get();
    }

    /**
     * 銷燬階段移除上下文對象的軟應用
     */
    public void detach(){
        if (mWeakReferInitActivity!=null){
            mWeakReferInitActivity.clear();
        }
    }
}

上下文在init或者attach階段傳入,使用軟應用形式持有,在銷燬的時候再執行對應的detach將軟應用移除。那我再槓,你這裏用的是activity,是短生命週期的,那我就不,我是個sdk或者庫,我需要貫穿整個生命週期呢?一樣鴨,在application類那裏去attach和detach即可

總結

以上就是常見的單例使用,雙重檢查常見,但容器和靜態內部類少見,枚舉比較簡約但要少用。而關於單例持有context的問題,避免類直接持有上下文,儘可能使用軟引用。在銷燬時候移除對上下文的軟引用再置空對象。

參考內容

https://www.jianshu.com/p/94e0f9ab3f1d

https://juejin.im/post/5b50b0dd6fb9a04f932ff53f

http://www.cnblogs.com/garryfu/p/7976546.html

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