設(shè)計(jì)模式之單例模式——對(duì)象創(chuàng)建型模式

前言

本文主要參考 那些年,我們一起寫(xiě)過(guò)的“單例模式”。

何為單例模式?

顧名思義,單例模式就是保證一個(gè)類(lèi)僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。通常我們可以讓一個(gè)全局變量使得一個(gè)對(duì)象被訪問(wèn),但它不能防止你實(shí)例化多個(gè)對(duì)象。一個(gè)最好的辦法就是,讓類(lèi)自身負(fù)責(zé)保存它的唯一實(shí)例。這個(gè)類(lèi)可以保證沒(méi)有其他實(shí)例可以被創(chuàng)建,并且它可以提供一個(gè)訪問(wèn)實(shí)例的方法。

結(jié)構(gòu)

單例模式結(jié)構(gòu)圖

適用情況

  1. 當(dāng)類(lèi)只能有一個(gè)實(shí)例而且客戶(hù)可以從一個(gè)眾所周知的訪問(wèn)點(diǎn)訪問(wèn)它時(shí)。
  2. 產(chǎn)生某對(duì)象會(huì)消耗過(guò)多的資源,為避免頻繁地創(chuàng)建與銷(xiāo)毀對(duì)象對(duì)資源的浪費(fèi)。如對(duì)數(shù)據(jù)庫(kù)的操作、訪問(wèn)IO、線程池、網(wǎng)絡(luò)請(qǐng)求等。

單例模式的優(yōu)缺點(diǎn)

  • 優(yōu)點(diǎn):可以減少系統(tǒng)內(nèi)存開(kāi)支,減少系統(tǒng)性能開(kāi)銷(xiāo),避免對(duì)資源的多重占用、同時(shí)操作。
  • 缺點(diǎn):擴(kuò)展困難,容易引發(fā)內(nèi)存泄露,測(cè)試?yán)щy,一定程度上違背了單一職責(zé)原則,進(jìn)程被殺死時(shí)可能狀態(tài)不一致問(wèn)題。

單例的各種實(shí)現(xiàn)

單例模式按加載時(shí)機(jī)可以分為:餓漢模式和懶漢模式;按實(shí)現(xiàn)的方式有雙重檢查加鎖方式,內(nèi)部類(lèi)方式、枚舉方式以及通過(guò)Map容器來(lái)管理單例的模式。作為一個(gè)單例,首先要確保的就是實(shí)例的“唯一性”,但是有很多因素如多線程、序列化、反射、克隆等因素會(huì)導(dǎo)致“唯一性”失效。其中,多線程問(wèn)題尤為突出。所以,我們應(yīng)該保證無(wú)論是在單線程還是多線程下單例模式都是可以運(yùn)行的。

  1. 懶漢式線程不安全方式

     public class Singleton{
         private static Singleton instance;
    
         //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化
         private Singleton() {
         }
    
         //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例
         public static Singleton getInstance(){
             if(instance == null){
                 instance = new Singleton();
             }
             return instance;
         }
     }
    

這種寫(xiě)法只能在單線程中使用。如果是多線程,可能發(fā)生一個(gè)線程通過(guò)并進(jìn)入了if(instance == null)判斷語(yǔ)句快,但還未來(lái)得及創(chuàng)建新的實(shí)例時(shí),另一個(gè)線程也通過(guò)了這個(gè)判斷語(yǔ)句,兩個(gè)線程最終都進(jìn)行了創(chuàng)建,導(dǎo)致了多個(gè)實(shí)例的產(chǎn)生。所以這種方式在多線程下不適用。

  1. 線程安全效率低方式

     public class Singleton {
         private static Singleton instance;
     
         //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化
         private Singleton() {
         }
     
         //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例
         public static synchronized Singleton getInstance() {
             //通過(guò)加上synchronized修飾符解決多線程不安全問(wèn)題
             if (instance == null) {
                 instance = new Singleton();
             }
             return instance;
         }
     }
    

這種方式雖然解決了線程安全問(wèn)題,但是這樣迫使每個(gè)線程在進(jìn)入這個(gè)方法之前,要先等待其他的線程離開(kāi)該方法,即不會(huì)有兩個(gè)線程同時(shí)進(jìn)入此方法進(jìn)行 new Singleton(),從而保證了單例的有效性。但是當(dāng)每個(gè)線程每次執(zhí)行g(shù)etInstance()方法獲取類(lèi)的實(shí)例時(shí),都會(huì)進(jìn)行同步。而事實(shí)上當(dāng)實(shí)例創(chuàng)建完成后,同步就變?yōu)椴槐匾拈_(kāi)銷(xiāo)了,這樣做在高并發(fā)下必然會(huì)拖垮性能。

  1. 同步代碼塊方式

     public class Singleton {
         private static Singleton instance;
     
         //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化
         private Singleton() {
         }
     
         //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例
         public static Singleton getInstance() {
             if (instance == null) {
                 //僅同步實(shí)例化的代碼塊
                 synchronized (Singleton.class){
                     instance = new Singleton();
                 }
             }
             return instance;
         }
     }
    

但是這種同步并不能做到線程安全,同最初的懶漢模式一個(gè)道理,它可能產(chǎn)生多個(gè)實(shí)例,所以亦不可行。我們必須再增加一個(gè)單例不為空的判斷來(lái)保證線程安全,也就是所謂的“雙重檢查鎖定(Double Check Lock(DCL))”

  1. 雙重檢查鎖定(Double Check Lock(DCL))方式

     public class Singleton {
         //注意此處的volatile修飾符
         //Java編譯器允許處理器亂序執(zhí)行,會(huì)有DCL失效的問(wèn)題
         //JDK大于等于1.5的版本,具體化了volatile關(guān)鍵字,定義時(shí)加上它可以保證執(zhí)行的順序(雖然會(huì)影響性能)
         //從而單例起效
         private static volatile Singleton instance;
     
         //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化
         private Singleton() {
         }
     
         //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例
         public static Singleton getInstance() {
             if (instance == null) {
                 //第一次check,避免不必要的同步
                 synchronized (Singleton.class) { //同步
                     if (instance == null) {
                         //第二次check,保證線程安全
                         instance = new Singleton();
                     }
                 }
             }
             return instance;
         }
     }
    

此方法的“Double-Check”體現(xiàn)了兩次 if(instance == null)的檢查,這樣既同步代碼塊保證線程安全,同時(shí)實(shí)例化的代碼只會(huì)執(zhí)行一次,實(shí)例化后同步不會(huì)再被執(zhí)行,從而提高效率。
雙重檢查鎖定(DCL)方式也延遲加載的,它唯一的問(wèn)題是Java編譯器允許處理器亂序執(zhí)行,在JDK版本低于1.5會(huì)有DCL失效的問(wèn)題。在版本大于等于1.5的JDK上,只需在定義單例時(shí)加上volatile關(guān)鍵字,即可保證執(zhí)行的順序,從而使單例起效。

  1. 延遲加載的靜態(tài)內(nèi)部類(lèi)

     public class Singleton {
     
         private Singleton(){}
     
         public static final Singleton getInstance(){
             return SingletonHolder.INSTANCE;
         }
     
         private static class SingletonHolder{
             private static final Singleton INSTANCE = new Singleton();
         }
     }
    

靜態(tài)內(nèi)部類(lèi)利用了classloader的機(jī)制來(lái)保證初始化 instance 時(shí)只會(huì)有一個(gè),這是因?yàn)樘摂M機(jī)會(huì)保證一個(gè)類(lèi)的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類(lèi),那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類(lèi)的<clinit>方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行 <clinit>() 方法完畢。如果在一個(gè)類(lèi)的 <clinit>() 方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)線程阻塞,這在實(shí)際應(yīng)用中往往是很隱蔽的。需要注意的是,其他線程雖然會(huì)被阻塞,但如果執(zhí)行 <clinit>() 方法的那條線程退出 <clinit>() 方法后,其他線程喚醒之后不會(huì)再次進(jìn)入 <clinit>() 方法。同一個(gè)類(lèi)加載器下,一個(gè)類(lèi)型只會(huì)初始化一次。

需要注意的是,雖然它的名字中有“靜態(tài)”兩字,但它屬于“懶漢模式”的。這種方式的Singleton類(lèi)被加載時(shí),因?yàn)閮?nèi)部靜態(tài)類(lèi)是要在有引用了之后才會(huì)裝載進(jìn)內(nèi)存,所以在第一次調(diào)用 getInstance()之前,換言之,只要 SingletonHolder 類(lèi)還沒(méi)有被主動(dòng)使用,instance 就不會(huì)被初始化。只有在顯示調(diào)用getInstance()方法時(shí),產(chǎn)生了對(duì)SingleHolder的引用才會(huì)加載SingltonHolder類(lèi),從而實(shí)例化對(duì)象。

  1. 餓漢加載方式

     //優(yōu)點(diǎn)是比較簡(jiǎn)潔
     public class Singleton{
         //注意這里用的是public而不是private,因此無(wú)需getInstance()方法,可以直接
         //拿到instance實(shí)例
         //此方法的final關(guān)鍵詞來(lái)確保每次返回的都是同一個(gè)對(duì)象的引用,私有的構(gòu)造方法函數(shù)
         //也只會(huì)被調(diào)用一次
         private Singleton(){}
         public static final Singleton instance = new Singleton();
     }
    
     //Singleton with static factory
     //現(xiàn)代的JVM基本都內(nèi)嵌了對(duì)static factory
     //方法的調(diào)用,使得第一種public field方式不再有優(yōu)勢(shì)
     //此方法更靈活,只需修改getInstance的返回邏輯,而不需要
     //改變API就可以將類(lèi)改為非單例類(lèi)
     public class Singleton {
         private Singleton(){}
         private static final Singleton instance = new Singleton();
         public static Singleton getInstance(){
             return instance;
         }
     }
    
     public class Singleton {
         private Singleton() {
         }
     
         private static Singleton instance = null;
         static {
             instance = new Singleton();
         }
         
         public static Singleton getInstance(){
             return instance;
         }
     }
    
      public class Singleton {
         private Singleton() {
         }
    
         private static Singleton instance = null;
         static {
             instance = new Singleton();
         }
    
         public static Singleton getInstance(){
             return instance;
         }
     }
    

這三種方式差別不大,都依賴(lài)JVM在類(lèi)加載時(shí)就完成唯一對(duì)象的實(shí)例化,基于類(lèi)加載的機(jī)制,它們天生就是線程安全的,所以都是可行的,第二種更易于理解比較常見(jiàn)。

  1. 枚舉方式 關(guān)于枚舉

     public enum  Singleton {
         INSTANCE;
         //枚舉同Java中的普通Class一樣,能夠有自己的成員變量和方法
         public void doSomething(){
             System.out.println("Do whatever you want");
         }
     }
    

枚舉類(lèi)型時(shí)有“實(shí)例控制”的類(lèi),確保了不會(huì)同時(shí)有兩個(gè)實(shí)例,即當(dāng)且僅當(dāng) a=b 時(shí) a.eaquals(b) ,用戶(hù)也可以用 == 操作符來(lái)代替 equals(Object) 方法來(lái)提供效率。使用枚舉來(lái)實(shí)現(xiàn)單例還可以不用getInstance()方法(當(dāng)然,如果你想要適應(yīng)大家的習(xí)慣用法,加上 getInstance()方法也是可以的),直接通過(guò)Singeton.INSTANCE 來(lái)拿取實(shí)例。枚舉類(lèi)是在第一次訪問(wèn)時(shí)才被實(shí)例化,是懶加載的。它寫(xiě)法簡(jiǎn)單,并確保了在任何情況下(包括反序列化,反射,克?。┫露际且粋€(gè)單例。不過(guò)枚舉是在JDK1.5之后才加入的特性。

其他需要注意的對(duì)單例模式的破壞

  1. 除了多線程,序列化也可能破壞單例模式一個(gè)實(shí)例的要求。二是實(shí)現(xiàn)對(duì)象數(shù)據(jù)的遠(yuǎn)程傳輸。當(dāng)單例對(duì)象有必要實(shí)現(xiàn)Serializable接口時(shí),即使將其構(gòu)造函數(shù)設(shè)為私有,在它反序列化時(shí)依然會(huì)通過(guò)特殊的途徑再創(chuàng)建類(lèi)的一個(gè)新的實(shí)例,相當(dāng)于調(diào)用了該類(lèi)的的構(gòu)造函數(shù)有效地獲得除了一個(gè)新的實(shí)例。

     public class Singleton implements Serializable{
         private static Singleton instance = new Singleton();
         private Singleton(){}
         public static Singleton getInstance(){
             return instance;
         }
     
         public static void main(String[] args) {
             Singleton instance1 = Singleton.getInstance();
             Singleton instance2 = Singleton.getInstance();
             System.out.println("normal:" + (instance1 == instance2));
             try {
                 //序列化
                 File file = new File("tt.txt");
                 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
                 oos.writeObject(instance1);
                 oos.close();
                 //反序列化
                 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                 Singleton instance3 = (Singleton) ois.readObject();
                 System.out.println("deserialize:" + (instance1 == instance3));
             } catch (FileNotFoundException e) {
                 e.printStackTrace();
             } catch (IOException e) {
                 e.printStackTrace();
             } catch (ClassNotFoundException e) {
                 e.printStackTrace();
             }
         }
     }
    

輸出如下:
normal:true
deserialize:false

要比避免單例對(duì)象在反序列化重新生成對(duì)象,則在implements Serializable的同時(shí)應(yīng)該實(shí)現(xiàn)readResolve()方法,并在其中保證反序列化的時(shí)候獲得原來(lái)的對(duì)象。
(注:readResolve()是反序列化操作提供的一個(gè)很特別的鉤子函數(shù),它在從流中讀取對(duì)象的readObject(ObjectInputStream)方法之后被調(diào)用,可以讓開(kāi)發(fā)人員控制對(duì)象的反序列化。

    public class Singleton implements Serializable{
            ......
            private Object readResolve(){
                return instance; //默認(rèn)返回 instance 對(duì)象,而不是重新生成一個(gè)新的對(duì)象
            }
            ......
    }

單例有效。

2.反射
除了多線程、反序列化以外,反射也會(huì)對(duì)單例造成破壞。反射可以通過(guò)setAccessible(true)來(lái)繞過(guò) private 機(jī)制,從而調(diào)用到類(lèi)的私有構(gòu)造函數(shù)創(chuàng)建對(duì)象。如下代碼所示:

public class Singleton {
        
            private static Singleton instance = new Singleton();
        
            private Singleton() {
            }
        
            public static Singleton getInstance() {
                return instance;
            }
        
            public static void main(String[] args) {
                Singleton getInstance1 = Singleton.getInstance();
                Singleton getInstance2 = Singleton.getInstance();
                System.out.println("Is singleton pattern normally valid: " + (getInstance1 == getInstance2));
                try {
                   /* Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.designpatterns.Singleton");
                    Constructor<Singleton> constructor = clazz.getConstructor(null); //獲得無(wú)參構(gòu)造函數(shù)*/
                    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
                    constructor.setAccessible(true); //跳過(guò)權(quán)限檢查,可以訪問(wèn)私有的構(gòu)造函數(shù)
                    Singleton refInstance3 = constructor.newInstance();
                    System.out.println("Is single pattern valid for Reflection: " + (getInstance1 == refInstance3));
                } catch (Exception e) {
                    e.printStackTrace();
                }
        
            }
        
        }

將會(huì)打?。?/p>

    Is singleton pattern normally valid: true
    Is single pattern valid for Reflection: false

說(shuō)明使用反射利用私有構(gòu)造器也是可以破壞單例的,要防止此情況發(fā)生,可以在私有的構(gòu)造器中加一個(gè)判斷,需要?jiǎng)?chuàng)建的對(duì)象不存在就創(chuàng)建;存在則說(shuō)明是第二次調(diào)用,拋出 RuntimeException 提示。代碼如下

    public class Singleton{
            ......
            private Singleton(){
                    //也可以在這里使用 flag 或者 計(jì)數(shù)器 count 來(lái)判斷
                    if(null  != instance){
                            throw new RuntimeException("Cann't construct a Singleton more than once!");
                    }
            }
    }

同反序列化相似,枚舉的方式也可以杜絕反射的破壞。當(dāng)我們通過(guò)反射方式來(lái)創(chuàng)建枚舉類(lèi)型的實(shí)例時(shí),會(huì)拋出異常。

  1. 克隆
    clone()是 Object 的方法,每一個(gè)對(duì)象都是 Object 的子類(lèi),都有 clone() 方法。 clone() 方法并不是調(diào)用構(gòu)造函數(shù)來(lái)創(chuàng)建對(duì)象,而是直接拷貝內(nèi)存區(qū)域。因此當(dāng)我們的單例對(duì)象實(shí)現(xiàn)了 Cloneable 接口時(shí),盡管其構(gòu)造函數(shù)時(shí)私有的,仍可以通過(guò)克隆來(lái)創(chuàng)建一個(gè)新對(duì)象,單例模式也相應(yīng)的失效了。
public class Singleton implements Cloneable{
        private static Singleton instance = new Singleton();
        private Singleton(){}
        public static Singleton getInstance(){
            return instance;
        }
    
        public static void main(String[] args) {
            Singleton getInstance1 = Singleton.getInstance();
            Singleton getInstance2 = Singleton.getInstance();
            System.out.println("Is singleton pattern normally valid: " + (getInstance1 == getInstance2));
            try {
                Singleton cloneInstance3 = (Singleton) getInstance1.clone();
                System.out.println("Is singleton pattern valid for clone: " + (getInstance1 == cloneInstance3));
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
        }
    }

輸出為:

    Is singleton pattern normally valid: true
    Is singleton pattern valid for clone: false

所以單例模式是不可以實(shí)現(xiàn) Cloneable 接口的,這與 Singleton 模式的初衷相違背。那要如何阻止使用 clone() 方法創(chuàng)建單例實(shí)例的另一個(gè)實(shí)例?可以 override 它的 clone() 方法,使其拋出異常。(也許你想問(wèn)既然知道了某個(gè)類(lèi)是單例且單例不應(yīng)該實(shí)現(xiàn) Cloneable 接口,那不實(shí)現(xiàn)該接口不就可以了嗎?事實(shí)上盡管很少見(jiàn),但有時(shí)候單例類(lèi)可以繼承其他類(lèi),如果父類(lèi)實(shí)現(xiàn)了 clone() 方法的話(huà),就必須在我們的單例中重寫(xiě)clone() 方法來(lái)阻止對(duì)單例的破壞。)

    public class Singleton implements Cloneable{
            ......
            @Override
            protected Object clone() throws CloneNotSupportException{
                    throw new CloneNotSupportException();
            }
            ......
    }

P.S. Enum 是沒(méi)有 clone() 方法的

登記式單例——使用Map容器管理單例模式

采用Map容器來(lái)統(tǒng)一管理這些單例,使用時(shí)通過(guò)統(tǒng)一的接口來(lái)獲取某個(gè)單例。將一組單例類(lèi)型注入到一個(gè)統(tǒng)一的管理類(lèi)中來(lái)維護(hù),即將這些實(shí)例存放在一個(gè)Map登記簿中,在使用時(shí)則根據(jù) key 來(lái)獲取對(duì)象對(duì)應(yīng)類(lèi)型的單例對(duì)象。對(duì)于已經(jīng)登記過(guò)的實(shí)例,從 Map 直接返回實(shí)例;對(duì)于沒(méi)有登記的,則先登記再返回。從而在對(duì)用戶(hù)隱藏具體實(shí)現(xiàn)、降低代碼耦合度的同時(shí),也降低了用戶(hù)的使用成本。簡(jiǎn)易版代碼實(shí)現(xiàn)如下

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

    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);
    }
}

Android 的系統(tǒng)核心服務(wù)就是如上形式存在的,以達(dá)到減少資源消耗的目的。其中最為大家所熟知的服務(wù)有 LayoutInflater Service,它就是在虛擬機(jī)第一次加載ContextImpl 類(lèi)時(shí),以單例形式注冊(cè)到系統(tǒng)中的一個(gè)服務(wù),其他系統(tǒng)級(jí)的服務(wù)還有:WindowManagerService、ActivityManagerService 等。JVM 第一次加載調(diào)用 ContextImpl 的 registerService()方法,將這些服務(wù)以鍵值對(duì)的形式(以service name 為鍵,值則是對(duì)應(yīng)的ServiceFetcher)存儲(chǔ)在一個(gè)HashMap中,要使用時(shí)通過(guò)key拿到所需的 ServiceFetcher 后,再通過(guò) ServiceFetcher 的 getService()方法來(lái)獲取具體的服務(wù)對(duì)象。在第一次使用服務(wù)時(shí),SercviceFetcher 調(diào)用 createService()方法創(chuàng)建服務(wù)對(duì)象,并緩存到一個(gè)列表中,下次再取時(shí)就可以直接從緩存中獲取,無(wú)需重復(fù)創(chuàng)建對(duì)象,從而實(shí)現(xiàn)單例的效果。

關(guān)于單例模式的其他問(wèn)題(Q & A)

  1. 還有其他情況會(huì)使單例模式失效嗎?
    上述的所有討論都是基于一個(gè)類(lèi)加載器(class loader)的情況。由于每個(gè)類(lèi)加載器有各自的命名空間, static 關(guān)鍵詞的作用范圍也不是整個(gè) JVM,而之到類(lèi)加載器,即不同的類(lèi)加載器可以加載同一個(gè)類(lèi)。所以當(dāng)一個(gè)工程下面存在不止一個(gè)類(lèi)加載器時(shí),整個(gè)程序中同一個(gè)類(lèi)就可能被加載多次,如果這是個(gè)單例類(lèi)就會(huì)產(chǎn)生多個(gè)單例并存失效的現(xiàn)象,并要指定同一個(gè)類(lèi)加載器。

  2. 單例的構(gòu)造函數(shù)時(shí)私有的,那還能不能繼承單例?
    單例是不適合被繼承的,要繼承單例就要將構(gòu)造函數(shù)改成公開(kāi)的或受保護(hù)的(僅考慮Java中的情況),這就會(huì)導(dǎo)致:
    1)別的類(lèi)也可以實(shí)例化它了,無(wú)法確保保實(shí)例“獨(dú)一無(wú)二”,這顯然有違單例的設(shè)計(jì)理念。
    2)因?yàn)閱卫膶?shí)例是使用的靜態(tài)變量,所有的派生類(lèi)事實(shí)上是共享同一個(gè)實(shí)例變量的,這種情況下要想讓子類(lèi)們維護(hù)正確的狀態(tài),順利工作,基類(lèi)就不得不實(shí)現(xiàn)注冊(cè)表功能了。

  3. 單例有沒(méi)有違反“單一責(zé)任原則”?
    單例確實(shí)承擔(dān)了兩個(gè)責(zé)任,它不僅僅負(fù)責(zé)管理自己的實(shí)例并提供全局訪問(wèn),還要處理應(yīng)用程序的某個(gè)業(yè)務(wù)邏輯。但是有類(lèi)來(lái)管理自己的實(shí)例的方式可以讓整體設(shè)計(jì)更簡(jiǎn)單易懂。
    當(dāng)然在代碼繁復(fù)的情況下優(yōu)化你的設(shè)計(jì),讓單例類(lèi)專(zhuān)注于自己的業(yè)務(wù)責(zé)任,將它的實(shí)例化以及對(duì)對(duì)象個(gè)數(shù)的控制封裝在一個(gè)工廠類(lèi)或生成器中,也是較好的解決方法。

  4. 是否可以把一個(gè)類(lèi)的所有方法和變量都定義為靜態(tài)的,把此類(lèi)直接當(dāng)作單例來(lái)使用?
    事實(shí)上在最開(kāi)始討論過(guò)的,Java里的 java.lang.System 以及 java.lang.Math 類(lèi)都是這么做的,它們的全部方法都用 static 關(guān)鍵詞修飾,包裝起來(lái)提供類(lèi)級(jí)訪問(wèn)??梢钥吹?,Math 類(lèi)的把 java 基本類(lèi)型值運(yùn)算的相關(guān)方法組織了起來(lái),當(dāng)我們調(diào)用 Math 類(lèi)的某個(gè)類(lèi)方法時(shí),所要做的都只是數(shù)據(jù)操作,并不涉及到對(duì)象的狀態(tài),對(duì)這樣的工具類(lèi)來(lái)說(shuō)實(shí)例化是沒(méi)有任何意義的。
    靜態(tài)方法會(huì)比一般的單例更快,因?yàn)殪o態(tài)的綁定是在編譯期就進(jìn)行的。但是也要注意到,靜態(tài)初始化的控制權(quán)完全握在 Java 手上,當(dāng)涉及到很多類(lèi)時(shí),這么做可能會(huì)一起一些微妙而不易察覺(jué)的,和初始化次序有關(guān)的bug。除非絕對(duì)必要,確保一個(gè)對(duì)象只有一個(gè)實(shí)例,會(huì)比類(lèi)只有一個(gè)單例更保險(xiǎn)。

  5. 考慮技術(shù)實(shí)現(xiàn)時(shí),如何從單例模式和全局變量中作出選擇?
    全局變量雖然使用起來(lái)比較簡(jiǎn)單,但對(duì)于單例有如下缺點(diǎn):
    1) 全局變量只是提供了對(duì)象的全局靜態(tài)引用,但并不能確保只有一個(gè)實(shí)例。
    2) 全局變量時(shí)急切實(shí)例化的,在程序一開(kāi)始就創(chuàng)建好對(duì)象,對(duì)非常耗費(fèi)資源的對(duì)象,或是程序執(zhí)行過(guò)程中一直沒(méi)有用到的對(duì)象,都會(huì)形成浪費(fèi)。
    3) 靜態(tài)初始化時(shí)可能信息不完全,無(wú)法實(shí)例化一個(gè)對(duì)象。即可能需要使用到程序中稍后計(jì)算出來(lái)的值才能創(chuàng)建單例。
    4) 使用全局變量容易造成命名空間污染。

  6. 可以用單例對(duì)象 Application 來(lái)解決組件傳遞數(shù)據(jù)的問(wèn)題嗎?
    在 Android 應(yīng)用啟動(dòng)后、任意組件被創(chuàng)建前,系統(tǒng)會(huì)自動(dòng)為應(yīng)用創(chuàng)建一個(gè) Application 類(lèi)(或其子類(lèi))的對(duì)象,且只創(chuàng)建一個(gè)。從此它一直在那里,直到應(yīng)用的進(jìn)程被殺掉。所以雖然 Application 并沒(méi)有采用單例模式來(lái)實(shí)現(xiàn),但是由于它的生命周期由框架來(lái)控制,和整個(gè)應(yīng)用的保持一致,且確保了只有一個(gè),所以可以被看作是一個(gè)單例。

一個(gè) Android 應(yīng)用總有一些信息,譬如說(shuō)一次耗時(shí)計(jì)算的結(jié)果,需要被用在多個(gè)地方。如果將需要傳遞的對(duì)象塞到 intent 里或者存儲(chǔ)到數(shù)據(jù)庫(kù)里來(lái)進(jìn)行傳遞,存儲(chǔ)都要分別寫(xiě)代碼實(shí)現(xiàn),還是有點(diǎn)麻煩的。既然 Application (或繼承它的子類(lèi))對(duì)于 APP 中的所有 activity 和 service 都可見(jiàn),而且隨著 App 啟動(dòng),它自始至終都在那里,就不禁讓我們想到,何不利用 Application 來(lái)持有內(nèi)部變量,從而實(shí)現(xiàn)在各組件間傳遞、分享數(shù)據(jù)呢?這看上去方便又優(yōu)雅,但卻是完全錯(cuò)誤的一種做法!如果你使用了如上做法,那你的應(yīng)用最終要么因?yàn)槿〔坏綌?shù)據(jù)發(fā)生 NullPointerException 而崩潰,要么就是取到了錯(cuò)誤的數(shù)據(jù)。這是因?yàn)?Application 不會(huì)永遠(yuǎn)駐留在內(nèi)存里,隨著進(jìn)程被殺掉,Application 也會(huì)被銷(xiāo)毀,再次使用時(shí),它會(huì)被重新創(chuàng)建,它之前保存下來(lái)的所有狀態(tài)都會(huì)被重置。

要預(yù)防這個(gè)問(wèn)題,我們不能用 Applicaiton 對(duì)象來(lái)傳遞數(shù)據(jù),而是要:
1) 通過(guò)傳統(tǒng)的 intent 來(lái)顯示傳遞數(shù)據(jù)(將 Parcelable 或 Serializable 對(duì)象放入intent / Bundle.Parcelable 性能比 Serializable 快一個(gè)量級(jí),但是代碼實(shí)現(xiàn)要復(fù)雜一些)。
2) 重寫(xiě) onSaveInstanceState()以及 onRestoreInstanceState()方法,確保進(jìn)程被殺掉時(shí)保存了必須的應(yīng)用狀態(tài),從而在重新打開(kāi)時(shí)可以正確恢復(fù)現(xiàn)場(chǎng)。
3) 使用合適的方式將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)或硬盤(pán)。
4) 總是做判空保護(hù)和處理。

  1. 在 Android 中使用單例還有哪些需要注意的地方
    單例在 Android 中的生命周期等于應(yīng)用的生命周期,所以要特別小心它持有的對(duì)象是否會(huì)造成內(nèi)存泄露,所以最好僅傳遞給單例 Application Context。

附錄

雙重檢查鎖定(DCL)單例在JDK 1.5 之前版本失效原因解釋
在高并發(fā)環(huán)境,JDK 1.4 及更早版本下,雙重鎖定偶爾會(huì)失敗。其根本原因是,Java 中 new 一個(gè)對(duì)象并不是原子操作,編譯時(shí) singleton = new Singleton ; 語(yǔ)句會(huì)被轉(zhuǎn)成多條匯編指令,大致做了3件事情:
1) 給 Singleton 類(lèi)的實(shí)例分配內(nèi)存空間;
2) 調(diào)用私有的構(gòu)造函數(shù) Singleton(),初始化成員變量;
3) 將 singleton 對(duì)象指向分配的內(nèi)存(執(zhí)行玩此操作 singleton 就不是 null 了);
由于 Java 編譯器允許處理器亂序執(zhí)行,以及 JDK 1.5 之前的舊的 Java 內(nèi)存模型中 Cache、寄存器到主內(nèi)存回寫(xiě)順序的規(guī)定,上面步驟 2) 和 3) 的執(zhí)行順序是無(wú)法確定的,可能是 1) → 2) → 3) 也可能是 1) → 3) → 2) 。如果是后一種情況,在線程 A 執(zhí)行完步驟 3) 但還沒(méi)完成 2) 之前,被切換到線程 B 上,此時(shí)線程 B 對(duì) singleton 第1次判空結(jié)果為 false,直接取走了 singleton使用,但是構(gòu)造函數(shù)卻還沒(méi)有完成所有的初始化工作,就會(huì)出錯(cuò),也就是 DCL 失效問(wèn)題。
在 JDK 1.5的版本中具體化了 volatile 關(guān)鍵字,將其加在對(duì)象前就可以保證每次都是從主內(nèi)存中讀取對(duì)象,從而修復(fù)了 DCL 失效問(wèn)題。當(dāng)然,volatile 或多或少還是會(huì)影響到一些性能,但比起得到錯(cuò)誤的結(jié)果,犧牲這點(diǎn)性能還是值得的。

參考資料

[1] opalli. 那些年,我們一起寫(xiě)過(guò)的 “單例模式”[EB/OL]. [2017-03-09]. http://mp.weixin.qq.com/s/wEK3UcHjaHz1x-iXoW4_VQ.
[2] Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides. 設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)[M]. 李英軍等譯.北京:機(jī)械工業(yè)出版社,2009.
[3] 程杰. 大話(huà)設(shè)計(jì)模式[M]. 北京 : 清華大學(xué)出版社 , 2007.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容