Android中單例模式的幾個坑

DK_BurNIng發表於2019-02-12

首先來看這樣一個單例,稍微有點經驗的同學可能都會說,這樣的單例是非執行緒安全的。要加個volatile關鍵字才可以。

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

但是你要是問他,為什麼是非執行緒安全的單例就答不出來了。搞清楚這個問題其實 對我們的多執行緒理解是很有好處的。

我們首先明確一下對於jvm來說,完成對一個變數的寫操作 到底是如何進行的。

寫操作: (1)先把值寫入cpu的快取記憶體cache中。(2)然後再把這個cache中的值拷貝到ram(也就是我們的記憶體)中。

注意啊,對於一個寫操作來說,這個(1)(2) 可不是原子操作,很有可能(1)執行完畢以後,cpu又去幹了其他事情, 並沒有第一時間把cache的值 寫入到ram中。而我們讀操作,都是從ram中去讀取一個值的。

所以這裡我們可以想一下,如果是多執行緒場景的話,會有一些坑。

然後再說一個概念,對於 singleton=new Singleton(); 這一條語句來說,他顯然不是一條指令就可以完成的。

正常情況來說,我們要完成這條語句涉及到的指令大約如下:

1.申請一段堆記憶體空間

2.在這個堆記憶體空間中把我們需要的物件初始化完畢

3.把singleton這個引用指向我們的堆記憶體空間地址。

但是坑爹就坑爹在,虛擬機器會有一個指令重排序的概念。當虛擬機器發現單執行緒下 指令的順序變更不會導致結果異常的時候 就會觸發指令重排序的機制, 他會導致上述的 123順序發生變更,比如我們把順序改成132 你就會發現 結果還是一樣的。 (指令重排序的觸發機制準確的來說是happens before原則 有興趣的同學可以深挖)

如果發生132的執行順序 會發生什麼?

假設執行緒a 進入到了同步程式碼塊中,這個時候觸發了指令重排序,順序變成132,假設cpu這個時候執行了13。然後轉頭 去執行執行緒b,執行緒b 進入getInstance方法的時候,他發現singleton 不是null了,於是歡天喜地的return了, 但是要知道這個時候執行緒a的 2還沒執行,也就是說singleton雖然不是空,但是他指向的地址空間裡面啥都沒有,物件還沒有初始化。所以這是一個非常大的隱患,雖然他發生的概率極低,低到我現在都沒有復現過這種現象,但是依舊有概率。

那麼正確的寫法:

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

複製程式碼

有很多人就會說 volatile 這個關鍵字以後,singleton=new Singleton(); 就不會發生指令重排了,所以這麼做是正確的。

現在明確的告訴你,上面這個觀點是錯誤的

singleton=new Singleton(); 這條語句背後的指令依舊有概率發生指令重排,只不過 volatile修飾過以後,在 這條語句背後的 指令完全執行完畢以前,對singleton這個引用的讀操作全部被遮蔽了。

也就是說 132的執行順序依舊會發生,只不過 當執行完13 而2沒有執行的時候,volatile修飾過的這個變數,所有對他的讀操作 都會暫時遮蔽,等待2操作執行完以後,才會進行讀操作。

這才是volatile關鍵字加上去以後的作用。

android很多程式碼比如eventbus的單例就是用的上述寫法。

當然了,上述寫法是典型的懶漢寫法,所謂懶漢你就理解成用的時候才例項化,不用的話不例項化。

但是如果你的需求是這個單例無論在什麼情況下都會存在,你當然可以寫成餓漢,餓漢的寫法更簡單。

缺點就是他會一直佔用記憶體。餓漢寫法很多,我寫個最簡單的:

   class Singleton {
        //最簡單的寫法就是這個了,直接public就行
        public static final Singleton instance = new Singleton();

        private Singleton() {
        }

    }
複製程式碼

單例序列化會破壞物件唯一性嗎?

答案是會的:

package com.wuyue.test;

import java.io.*;

/**
 * Created by 16040657 on 2019/2/12.
 */
public class Test2 {


    public static void main(String args[]) {

        Singleton s1 = Singleton.instance;

        File f = new File("../test.txt");
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
            oos.writeObject(s1);
            oos.close();

            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
            Singleton s3 = (Singleton) ois.readObject();

            System.out.println("s1==s3:" + (s1 == s3));

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }


    }

    static class Singleton implements Serializable {
        //最簡單的寫法就是這個了,直接public就行
        public static final Singleton instance = new Singleton();

        private Singleton() {
        }

//        //這個方法就可以保證序列化和反序列化得到的物件是同一個了
//        private Object readResolve() {
//            return instance;
//        }

    }
}

複製程式碼

程式碼比較簡單,大家可以測試一下,s1和s3就是2個不同的物件,但是如果把註釋掉的readResolve方法放開的話,你就會發現 這個問題解決了,序列化和反序列化是同一個物件了。

對外部公開提供的sdk的單例要注意些什麼?

尤其是對於很多金融安全類的sdk來說,如果你這個裡面有單例的話,涉及到安全性要儘可能的不被業務方hook, 其中尤其要注意的就是 有人可能會利用反射來new一個物件,破壞單例

解決這個問題也不難,

 private Singleton() {
            //防止有人利用反射惡意修改
            if (null != instance) {
                throw new RuntimeException("dont construct more!");
            }

        }
複製程式碼

專案中的單例太多,如何有效管理?

其實就拿map管理就可以了,android裡面的 wms,ams 等等系統單例服務都是這樣的。你傳一個key進去 返回一個單例給你。

這個真的很有用哦,特別是大型工程,可以有效管理單例,文件輸出就簡單許多。

    static class SingletonManager {
        private static Map<String, Object> objectMap = new HashMap<>();

        private SingletonManager() {
        }

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

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

    }

複製程式碼

android中使用單例還要注意些什麼?

最主要的就是儘量不要利用單例模式儲存傳遞資料,因為app掛在後臺的時候程式會容易被殺掉,如果回到前臺再取這個單例裡的 資料很容易就取到個null,所以android中寫單例的原則就是:

原則上不允許用單例模式傳遞資料,如果一定要這麼做,請考慮資料恢復現場。

相關文章