單例模式,你會寫幾種?


定義:

  單例模式(singleton),保證一個類僅有一個實例,並且提供一個訪問它的全局訪問點。
  這句話很好理解,今天我們的重點也不在於如何解讀單例模式。
  在面試的過程中,往往會遇到考察手寫單例模式的場景,今天讓我們關注一下,寫單例模式的幾種方法。

餓漢式:

/**
 * 餓漢式.
 *
 * @author jialin.li
 * @date 2019-12-30 22:13
 */
public class Singleton {
    private Singleton() {
    }

    private static Singleton singleton = new Singleton();

    public Singleton getInstance(){
        return singleton;
    }
}
    • 餓漢式的特點是類初始化的時候,創建了該對象。
    • 由於類只會初始化一次,所以保證了對象只會被創建一次。
    • 同時將構造方法私有化,保證了沒有辦法從外部創建對象。
  這種方法的問題是,如果該實例從始至終都沒有被使用過,就會造成內存的浪費。

懶漢式:

/**
 * 懶漢式.
 *
 * @author jialin.li
 * @date 2019-12-30 22:13
 */
public class Singleton {
    private Singleton() {
    }

    private volatile Singleton singleton = null;

    public Singleton getInstance(){
        // 提高性能,降低線程進入臨界區的可能
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
  這種寫法又被成為 雙檢鎖模式,是一種實現單例模式的經典寫法。
  代碼中有兩處判空:
    1. 第一處判空,是為了提高性能,降低線程進入臨界區的可能性。
    2. 第二處判空是為了線程同步,假如沒有第二處判空,則可能兩個線程都通過了if(singleton==null)條件,這樣即使是臨界區內只有一個線程在執行,臨界區內的代碼也會被執行兩遍,這樣就會產生兩個對象,不符合單例模式。
  成員變量使用了volatile進行修飾,一方面是保證了對象在多線程環境下的可見性,另一方面是為了防止new Singleton()進行指令重排序而導致的並發問題。
  volatile關鍵字的作用兩個:
    1. 保證變量在線程之間的可見性(直接從主存中讀寫數據,不經過工作內存)
    2. 阻止編譯時和運行時的指令重排,編譯時JVM編譯器遵循內存屏障約束,運行時依賴CPU屏障來阻止指令重排。
  指令重排是指JVM在編譯Java代碼的時候,或者CPU在執行JVM字節碼的時候,對現有的指令順序進行重新排序。
  指令重排的目的是為了在不改變程序執行結果的前提下,優化程序的運行效率。需要注意的是,這里所說的不改變執行結果,指的是不改變單線程下的程序執行結果。
這里不太好懂,舉一個例子,正常的new Singleton()創建步驟是:
  1. 開辟一塊內存空間
  2. 創建對象
  3. 將對象的地址存入引用變量
  經過指令重排后,可能變成了:
  1. 開辟一塊內存空間
  2. 將對象的地址存入引用變量
  3. 創建對象

  假設發生了指令重排,線程A、B都執行這段代碼,線程A執行到了new Singleton()的步驟2,此時還沒有創建對象,這個時候發生了線程的切換。線程B開始執行,這個時候線程B還可以通過if(singleton == null)的判斷,因為線程A中的singleton只是指向了一個空的內存地址,這個時候線程B創建出了一個Singleton對象,當線程切換成A時,線程A仍執行了new Singleton()的步驟3,此時創建了2個Singleton對象,不符合單例模式。

靜態內部類單例模式:

/**
 * 靜態內部類單例模式.
 *
 * @author jialin.li
 * @date 2019-12-30 22:13
 */
public class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return Inner.singleton;
    }

    private static class Inner {
        private static Singleton singleton = new Singleton();
    }
}
  這里利用的是內部類的特性,只有第一次調用getInstance方法的時候,虛擬機才會加載Inner並初始化singleton,並且只初始化一次。這種方法也是一種懶漢式的寫法,只有在需要的時候,才創建對象。

枚舉單例模式

  枚舉類也是一種單例模式
public enum Singleton  {
    INSTANCE 
 
    //doSomething 該實例支持的行為
      
    //可以省略此方法,通過Singleton.INSTANCE進行操作
    public static Singleton get Instance() {
        return Singleton.INSTANCE;
    }
}
這種寫法較為簡單,並且沒有辦法用反射的方式,創建對象。但是可讀性較差。

期待您的關注、推薦、收藏,同時也期待您的糾錯和批評。


免責聲明!

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



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