簡說設計模式——單例模式


一、什么是單例模式

  大家學操作系統的時候應該知道,當多個進程或線程同時操作一個文件時,只有一個能訪問;java中類似的例子也有很多,比如多線程中我們最常用的鎖,保證了多線程同時對一個方法或對象操作時只有一個能夠訪問。單例模式就是如此,我們給出它的定義。

  單例模式(Singleton),保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。UML結構圖如下:

  

       其中,Singleton類定義了一個getInstance操作,允許客戶端訪問它的唯一實例,getInstance是一個靜態方法,主要負責創建自己的唯一實例。而對象的聲明是private的,其他類無法訪問到,只能通過getInstance()方法訪問其唯一實例。

 1 public class Singleton {
 2 
 3     private static Singleton instance;
 4     
 5     //限制產生多個對象
 6     private Singleton() {
 7     }
 8     
 9     //通過方法獲取實例對象
10     public static Singleton getInstance() {
11         if(instance == null) {
12             instance = new Singleton();
13         }
14         
15         return instance;
16     }
17     
18 }

       上述代碼就是一個單例模式,首先聲明了靜態私有的一個對象,並通過getInstance方法返回該對象。如果該對象已經存在,則直接返回該對象,若不存在,則實例化后返回該對象。下面看一段代碼:

public class Client {
    
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        
        if(instance1 == instance2) {
            System.out.println("兩個對象是相同的實例");
        }
    }
    
}
       我們運行上述程序,控制台輸出了”兩個對象是相同的實例“,這說明singleton1和singleton2是相同的實例,也即一個類僅有一個實例,符合單例模式的定義。

二、單例模式的應用

    1. 何時使用

  • 當我們想控制實例數目,節省系統資源時,可以使用單例模式。

    2. 優點

  • 內存中只有一個實例,減少了內存開支,尤其一個對象需要頻繁地創建銷毀,而此時性能又無法優化時,單例模式的優勢就非常明顯。
  • 避免對資源的多重占用(比如寫文件操作,只有一個實例時,避免對同一個資源文件同時寫操作),簡單來說就是對唯一實例的受控訪問。

    3. 缺點

  • 沒有接口,不能繼承,與單一職責沖突。

    4. 使用場景

  • 要求生成唯一序列號的環境。
  • 在整個項目中有一個共享訪問點或共享數據(如web頁面上的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來即可)。
  • 創建一個對象需要消耗的資源過多時(如訪問I/O和數據庫等資源)。

    5. 應用實例

  • 一個黨只有一個主席/一個國家只有一個國王/一個皇朝只有一個皇帝。
  • 計划生育。
  • 多個進程或線程同時操作一個文件的現象。

三、高並發下的單例模式

       需要注意的是,在高並發情況下,要注意單例模式線程同步的問題。單例模式有幾種不同的實現方式,如上方的代碼就需要考慮線程同步。

    1. 懶漢式

       所謂懶漢式單例,就是通過在上述代碼中增加synchronized關鍵字來實現。

public class Singleton {

    private volatile static Singleton instance;
    private static Object syncRoot = new Object();
    
    private Singleton() {
    }

    public static Singleton getInstance() {
        //雙重鎖定
        if(instance == null) {
            synchronized (syncRoot) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        
        return instance;
    }
    
}

       這里使用了雙重鎖定(Double-Check Locking),這樣可以不用讓線程每次都加鎖,而只是在實例未被創建的時候再枷鎖處理,同時也能保證多線程的安全。

      而這里判斷了兩次instance實例是否存在的原因是,當instance為null時,並且同時有兩個線程調用getInstance()方法時,它們都可以通過第一重instance==null的判斷,然后由於lock機制,這兩個線程則只有一個進入,另一個在外排隊等候,必須要其中一個進入並出來后,另一個才能進入,而此時如果沒有了第二重排序,則第一個線程創建了實例,而第二個線程還是可以繼續再創建新的實例,就沒有達到單例的目的。

       這里還需要注意一個問題,第三行中加入了volatile關鍵字,這里如果不加volatile可能會出現一個錯誤,即當代碼讀取到第11行的判斷語句時,如果instance不為null時,instance引用的對象有可能還沒有完成初始化,線程將訪問到一個還未初始化的對象。究其原因是因為代碼第14行,創建了一個對象,此代碼可分解為三行偽代碼,即分配對象的內存空間、初始化對象、設置instance指向剛分配的內存地址,分別記為1、2、3,在2和3之間,可能會被重排序,重排序后初始化就變為了最后一步。因此,線程A的intra-thread semantics(所有線程在執行Java程序時必須遵守intra-thread semantics,它保證重排序不會改變單線程內的程序執行結果)沒有改變,但A2和A3的重排序將導致線程B判斷出instance不為空,線程B接下來將訪問instance引用的對象,此時,線程B將會訪問到一個還未初始化的對象。而使用volatile就可以實現線程安全的延遲初始化,本質時通過禁止2和3之間的重排序,來保證線程安全的延遲初始化。

    2. 餓漢式

       餓漢式單例就不會出現產生多個單例的情況,但它是在自己被加載時就將自己實例化,所以要提前占用系統資源。

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

    public static Singleton getInstance() {
        return instance;
    }
    
    //類中其他方法,盡量使static
    public static void dosomething() {
    }
    
}

    3. 靜態內部類

       這種方式與餓漢式一樣,同樣利用了類加載來保證只創建一個instance實例,因此不存在線程安全的問題,不一樣的是,它是在內部類里面去創建對象實例。這樣只要應用中不使用內部類,JVM就不會去加載這個單例類,也就不會創建單例對象,從而實現懶漢式延遲加載。

public class Singleton {

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

    4. 枚舉

       上面實現單例的方式都需要額外的工作來實現序列化,而且可以使用反射強行調用私有構造器。

       而枚舉很好的解決了這兩個問題,使用枚舉除了線程安全和防止反射調用構造器外,還提供了自動序列化機制,防止反序列化的時候創建新的對象。

public enum Singleton {

    instance;
    
    public static void dosomething() {
    }
    
}

       枚舉的客戶端寫法如下:

public class Client {
    
    public static void main(String[] args) {    
        //枚舉
        Singleton instance1 = Singleton.instance;
        Singleton instance2 = Singleton.instance;
        
        if(instance1 == instance2) {
            System.out.println("兩個對象是相同的實例");
        }
    }
    
}

四、單例模式的實現

       下面舉一個完整的例子,就以一個皇朝只有一個皇帝為例,假設當今是唐朝小李的天下,來看看怎么用單例模式實現。UML圖如下:

    1. 皇帝類(Emperor類)

public class Emperor {
    
    private static final Emperor EMPEROR = new Emperor();
    
    private Emperor() {
    }
    
    public static Emperor getEmperor() {
        return EMPEROR;
    }
    
    public static void say() {
        System.out.println("朕乃當今聖上小李");
    }

}

       這里使用的是餓漢式單例,這樣我們在加載類的時候就對對象進行了實例化操作,后續只需調用getEmperor()方法即可。

    2. 客戶端

public class Client {

    public static void main(String[] args) {
        //臣子朝拜
        for(int day=0; day<3 ;day++) {
            Emperor emperor = Emperor.getEmperor();
            emperor.say();
        }
    }
    
}

       客戶端部分寫了一個每日早朝的情況,臣子每日都要朝拜皇帝,今天朝拜的皇帝應該和昨天、前天的一樣,運行結果如下:

       運行結果表示,連續三天上朝的皇帝都是小李,這就是一個簡單的單例模式。

   

       源碼地址:https://gitee.com/adamjiangwh/GoF    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM