java中你的單例在裸奔嗎?

weixin_34337265發表於2017-07-20
1152047-34f09cf5cca8ea8d.jpg
星爺鎮樓.

在上一篇文章java中你確定用對單例了嗎?中提到單例可以被惡意的破壞,如序列化破壞和反射破壞單例的結構,好的,這個有點偏,確實在實際開發中基本也不會在意到這個問題,但是誰叫我們搞的是java,所以這個問題我們有必要知道下,這算是提高下自己的安全意識,有句古話是這樣說的,居安思危嘛.

好,請帶著歡樂的心情繼續往下看.

通過反射破解單例結構

java中你的單例是不是一直在裸奔,估計你用的是假的單例.
我們就使用普通懶漢式來做示例吧.

public class SingletonDemo6  implements Serializable{
    private static SingletonDemo6 s1;
  //普通懶漢式寫法
    public static synchronized SingletonDemo6 getInstance() {
        if (s1 == null) {
            s1 = new SingletonDemo6();
        }
        return s1;
    }

看看下面測試結果.
在正常情況下,沒毛病,輸出結果一毛一樣.

@Test
public  void test() throws Exception{
        SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
        SingletonDemo6 nomarlInstance2 = SingletonDemo6.getInstance();

//這兩個單例輸入的例項都是一樣
System.out.println(nomarlInstance1);
System.out.println(nomarlInstance2);

log:
com.relice.singleton.SingletonDemo6@5a10411
com.relice.singleton.SingletonDemo6@5a10411

當反射遇上單例

看下面反射破解單例的測試程式碼,輸出兩個不同的結果.

@Test
public  void test() throws Exception{
        SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();

         Class<SingletonDemo6> forName = (Class<SingletonDemo6>) Class
         .forName("com.relice.singleton.SingletonDemo6");
         Constructor<SingletonDemo6> c = forName.getDeclaredConstructor();
         //繞過許可權管理,獲取private
         c.setAccessible(true);
         //通過反射拿到`SingletonDemo6`的例項
         SingletonDemo6 reflectInsatnce = c.newInstance();
        
         // 兩者的輸出結是不一樣的
         System.out.println(nomarlInstance1);
         System.out.println(reflectInsatnce);

log:
com.relice.singleton.SingletonDemo6@5a10411
com.relice.singleton.SingletonDemo6@2ef1e4fa

如何解決這種問題?

大神說遇到問題不要急,先分析問題出現的原因.

  1. forName.getDeclaredConstructor();主要就是獲取無引數構造.
  2. 也就是說通過反射拿到了私有構造方法從而再次建立例項.

知道問題的原因那就好辦了.
我們可以在SingletonDemo6的構造方法裡做判斷,避免他再次建立例項.

// 解決反射 多獲取物件問題
        private SingletonDemo6() {
            if (s1 != null) {
                try {
                    throw new RuntimeException("禁止反射獲取物件");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

這樣如果有人想要通過反射破壞單例結構,那就會丟擲執行時異常.

log:
java.lang.RuntimeException: 禁止反射獲取物件 at
com.relice.singleton.SingletonDemo6.<init>(SingletonDemo6.java:24)

通過序列化破解單例結構

還是用SingletonDemo6來測試,通過序列化獲取到例項,得出了兩個不一樣的結果.
憋說話,繼續看問題.

@Test
public  void test() throws Exception{
        SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
        
    //把物件寫入檔案
        File file = new File(
                "/xxx/xxx/xxx/xxx/xxx/SingletonDemo/a.txt");
        FileOutputStream fos = new FileOutputStream(file);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(nomarlInstance1);
        oos.close();
        fos.close();
        
        //序列化把物件讀取
        FileInputStream fis = new FileInputStream(file);
        ObjectInputStream ois = new ObjectInputStream(fis);
        SingletonDemo6 serilizeInstance = (SingletonDemo6) ois.readObject();
        
        System.out.println(nomarlInstance1);
        System.out.println(serilizeInstance);
}   

log:
com.relice.singleton.SingletonDemo6@68de145
com.relice.singleton.SingletonDemo6@27fa135a

如何解決這種問題?

老規矩我們還是先分析.

  1. 在序列化裡我們可以通過流的方式將一個物件寫入記憶體中oos.writeObject,因此也就可以將這個物件從記憶體中讀取出來.
  2. 但是當序列化遇到單例問題就發生了,在讀取物件時jvm會重新給序列化物件分配地址.
  3. 因此我們要考慮的問題就是反序列化

解決方法:
當反序列化的時候:
JVM會呼叫readObject方法,將我們剛剛在writeObject方法序列化好的屬性,反序列化回來. 然後在readResolve方法中,我們也可以指定JVM返回我們特定的物件(不是剛剛序列化回來的物件). .該方法的分析見

private Object readResolve() throws ObjectStreamException{
     return SingletonDemo6.s1;
}

可能我們會考慮到一個問題,就是之前的反射不是在構造方法裡處理解決問題嗎,那是不是序列化也可以?
要知道序列化和反序列化,在java中是使用位元組碼技術生成物件,並不會執行構造器方法.

android開發中要注意的問題

接觸過android的都知道,在其中四大元件中就有三大元件是有生命週期的,生命週期最關鍵的的就是context,連基本的Activty 和 Service都是從Context派生出來的,也因為這生命週期讓android應用在使用者體驗上附上了一些生命氣息,,如視訊播放根據生命週期來處理播放狀態;如我們想邊聽音樂邊幹些別事情,這是Service的生命週期就有幫我們做到等..

我想說的就是.
android元件中的生命週期是尤其重要,因此我們要善待context,在處理或者使用到元件的生命週期時也要注意規範,提高容錯率.

Android開發 單例模式導致記憶體洩露

實際開發中用到最多的設計模式,如果單例設計模式認第二,我想沒有敢認第一的.如工具類,application類,配置檔案等.
不扯淡了,以工具類為例,存在記憶體洩露問題的一些程式碼片段像下面這樣:

public class Util {

    private Context mContext;
    private static Util mInstance;

    private Util(Context context) {
        this.mContext = context;
    }

    public static Util getInstance(Context context) {
        if (mInstance == null) {
            synchronized (Util.class) {
                if (mInstance == null) {
                    mInstance = new Util(context);
                }
            }
        }
        return mInstance;
    }
}

其實實際開發中排查問題和定位問題一直是佔據了大部分的工作時間,因此擁有一個好的開發方式可以減少很多不必要的時間浪費,這裡有篇關於使用android studio檢查記憶體洩漏的文章覺得不錯.

分析下問題:

  1. Util.getInstance(this);這個this使用的就是Activity的context.
  2. Activity的生命週期都是比較短暫的,當使用者切換頁面的時候基本都會把activity銷燬掉,因此貫穿整個生命週期的context類也會被相應的相會.
  3. Util.getInstance(mContext);在工具類裡封裝一些耗時的操作也是常見的,當Activity生命週期結束,但Util類裡面卻還存在A的引用 (mContext),這樣Activity佔用的記憶體就一直不能回收,而Activity的物件也不會再被使用.從而造成記憶體洩漏.

解決問題:

  1. 在Activity中,可以用Util.getInstance(getApplicationContext());Util.getInstance(getApplication());來代替。
    因為Application的生命週期是貫穿整個程式的,所以Util類持有它的引用,也不會造成記憶體洩露問題。

  2. 使用弱引用讓這個引用自動被回收
    弱引用也是用來描述非必需物件的,當JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的物件。

下面程式碼是使用了弱引用之後

public class WeakRUtil {
    private static Context mContext;
    private static WeakRUtil mInstance;

    private WeakRUtil(Context context) {
        this.mContext=context;
    }

    public static WeakRUtil getInstance(Context context) {
        if (mInstance == null) {
            WeakReference<Context> actWeakRF = new WeakReference<Context>(context);
            //通過get來獲取弱引用關聯物件,如果為null 則就是被回收了
            mContext = actWeakRF.get();

            synchronized (WeakRUtil.class) {
                if (mInstance == null) {
                    mInstance = new WeakRUtil(mContext);
                }
            }
        }
        return mInstance;
    }

    public void test() {
        System.out.println("util_test");
    }
}

在java中,用java.lang.ref.WeakReference類來表示。
當呼叫了System.gc(); 則即使記憶體足夠,該引用內的資料都會被回收;

好了,我們繼續總結下:

  1. 單例的優點就是提供了對唯一例項的受控訪問,減少記憶體分配,提高系統效能,也因為這個優點所以我們要避免單例被惡意的破壞掉了其結構.
  2. 單例在實際開發中經常使用到,而使用方法也是各種各種,為了讓程式碼有更好的健壯性,因此一些開發中的程式設計習慣要養成,避免如oom異常.

相關文章