設計模式之單例模式——對象創(chuàng)建型模式

前言

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

何為單例模式?

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

結構

單例模式結構圖

適用情況

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

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

  • 優(yōu)點:可以減少系統(tǒng)內存開支,減少系統(tǒng)性能開銷,避免對資源的多重占用、同時操作。
  • 缺點:擴展困難,容易引發(fā)內存泄露,測試困難,一定程度上違背了單一職責原則,進程被殺死時可能狀態(tài)不一致問題。

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

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

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

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

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

  1. 線程安全效率低方式

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

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

  1. 同步代碼塊方式

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

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

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

     public class Singleton {
         //注意此處的volatile修飾符
         //Java編譯器允許處理器亂序執(zhí)行,會有DCL失效的問題
         //JDK大于等于1.5的版本,具體化了volatile關鍵字,定義時加上它可以保證執(zhí)行的順序(雖然會影響性能)
         //從而單例起效
         private static volatile Singleton instance;
     
         //private的構造函數(shù),只能在本類內部實例化
         private Singleton() {
         }
     
         //通過此靜態(tài)方法提供全局獲取唯一可用對象的實例
         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)的檢查,這樣既同步代碼塊保證線程安全,同時實例化的代碼只會執(zhí)行一次,實例化后同步不會再被執(zhí)行,從而提高效率。
雙重檢查鎖定(DCL)方式也延遲加載的,它唯一的問題是Java編譯器允許處理器亂序執(zhí)行,在JDK版本低于1.5會有DCL失效的問題。在版本大于等于1.5的JDK上,只需在定義單例時加上volatile關鍵字,即可保證執(zhí)行的順序,從而使單例起效。

  1. 延遲加載的靜態(tà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)內部類利用了classloader的機制來保證初始化 instance 時只會有一個,這是因為虛擬機會保證一個類的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的<clinit>方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時很長的操作,就可能造成多個線程阻塞,這在實際應用中往往是很隱蔽的。需要注意的是,其他線程雖然會被阻塞,但如果執(zhí)行 <clinit>() 方法的那條線程退出 <clinit>() 方法后,其他線程喚醒之后不會再次進入 <clinit>() 方法。同一個類加載器下,一個類型只會初始化一次。

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

  1. 餓漢加載方式

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

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

  1. 枚舉方式 關于枚舉

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

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

其他需要注意的對單例模式的破壞

  1. 除了多線程,序列化也可能破壞單例模式一個實例的要求。二是實現(xiàn)對象數(shù)據(jù)的遠程傳輸。當單例對象有必要實現(xiàn)Serializable接口時,即使將其構造函數(shù)設為私有,在它反序列化時依然會通過特殊的途徑再創(chuàng)建類的一個新的實例,相當于調用了該類的的構造函數(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

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

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

單例有效。

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

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); //獲得無參構造函數(shù)*/
                    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
                    constructor.setAccessible(true); //跳過權限檢查,可以訪問私有的構造函數(shù)
                    Singleton refInstance3 = constructor.newInstance();
                    System.out.println("Is single pattern valid for Reflection: " + (getInstance1 == refInstance3));
                } catch (Exception e) {
                    e.printStackTrace();
                }
        
            }
        
        }

將會打?。?/p>

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

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

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

同反序列化相似,枚舉的方式也可以杜絕反射的破壞。當我們通過反射方式來創(chuàng)建枚舉類型的實例時,會拋出異常。

  1. 克隆
    clone()是 Object 的方法,每一個對象都是 Object 的子類,都有 clone() 方法。 clone() 方法并不是調用構造函數(shù)來創(chuàng)建對象,而是直接拷貝內存區(qū)域。因此當我們的單例對象實現(xiàn)了 Cloneable 接口時,盡管其構造函數(shù)時私有的,仍可以通過克隆來創(chuà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

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

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

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

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

采用Map容器來統(tǒng)一管理這些單例,使用時通過統(tǒng)一的接口來獲取某個單例。將一組單例類型注入到一個統(tǒng)一的管理類中來維護,即將這些實例存放在一個Map登記簿中,在使用時則根據(jù) key 來獲取對象對應類型的單例對象。對于已經(jīng)登記過的實例,從 Map 直接返回實例;對于沒有登記的,則先登記再返回。從而在對用戶隱藏具體實現(xiàn)、降低代碼耦合度的同時,也降低了用戶的使用成本。簡易版代碼實現(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)核心服務就是如上形式存在的,以達到減少資源消耗的目的。其中最為大家所熟知的服務有 LayoutInflater Service,它就是在虛擬機第一次加載ContextImpl 類時,以單例形式注冊到系統(tǒng)中的一個服務,其他系統(tǒng)級的服務還有:WindowManagerService、ActivityManagerService 等。JVM 第一次加載調用 ContextImpl 的 registerService()方法,將這些服務以鍵值對的形式(以service name 為鍵,值則是對應的ServiceFetcher)存儲在一個HashMap中,要使用時通過key拿到所需的 ServiceFetcher 后,再通過 ServiceFetcher 的 getService()方法來獲取具體的服務對象。在第一次使用服務時,SercviceFetcher 調用 createService()方法創(chuàng)建服務對象,并緩存到一個列表中,下次再取時就可以直接從緩存中獲取,無需重復創(chuàng)建對象,從而實現(xiàn)單例的效果。

關于單例模式的其他問題(Q & A)

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

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

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

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

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

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

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

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

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

附錄

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

參考資料

[1] opalli. 那些年,我們一起寫過的 “單例模式”[EB/OL]. [2017-03-09]. http://mp.weixin.qq.com/s/wEK3UcHjaHz1x-iXoW4_VQ.
[2] Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides. 設計模式:可復用面向對象軟件的基礎[M]. 李英軍等譯.北京:機械工業(yè)出版社,2009.
[3] 程杰. 大話設計模式[M]. 北京 : 清華大學出版社 , 2007.

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容