設計模式淺談 —— 單例模式

作者 tanghuailong

如果喜歡那就去做吧

單例模式

初探單例
  • 定義
    單例模式 也叫單子模式,是一種比較常見的軟件設計模式,在應用這個模式的時,單例模式的類確保只有一個實例的存在。因為在許多時候我們系統中只需要一個全局的實例對象,用來協調系統的整體行為。
  • 現狀
    單例模式是最容易理解的設計模式,但存在很嚴重的濫用現狀,有很多時候其實沒有必要關系到單例模式。所以這篇文章的目的不是要教你如何寫單例模式,而是要進行分析其中的優缺點,擇優而用
實現的分類

單例模式的實現,其實總共就分為兩種,一種為懶漢式,一種餓漢式。

提起單例模式的分類我又想起一個故事。今年初入職場面試的時候,面試的問我,你知道單例模式的懶漢模式和餓漢模式么。我當時的內心是,WTF? ,啥是懶漢?啥是餓漢。。。
后來才去查了一下,奧,這東西就叫懶漢呀,這特么不是懶加載么!

  • 餓漢式(eager initialization)
public class Singleton {
    //實例化singleton
    private static final Singleton instance = new Singleton();
   //定義為私有,確保外部不能使用此構造方法
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

上面的代碼就是餓漢式的實現,在加載類的時候,就進行了實例化,如果 private singleton(){} 里面包含比較笨重的操作時候,使用這種方式,會在一開始啟動的時候,比較浪費時間,帶來一種不好的體驗。另外代碼里需要注意的點,我給加上了注釋。

  • 懶漢式(lazy initialization)
  • 雙重驗證加鎖(Double Checked Locking)
public class Singleton {
  //一開始并未進行 實例化,實例化放在調用 getInstance()時候才實例化
    private static volatile Singleton instance = null;

    private Singleton() {   }

    public static Singleton getInstance() {
        // A 點
        if (instance == null) {
       //B 點
            synchronized (Singleton.class){
  //C 點
                if (instance == null) {
                    instance = new Singleton();
                }
                // 錯誤寫法(錯誤)
                // instance = new Singleton();
            }
        }
        return instance;
    }
}

說一下 雙重驗證加鎖 ,這個名字是我自己根據英文起的,記不起叫什么名字了,這里把需要注意的點說一下。
為什么要在getInstance()方法中判斷兩次是非為null?
答: 因為在多線程的情況下,進入A 點的可能是多個線程,即進入getInstance()方法的可能是多個線程,當進入B點的時候,可能是線程1先到了B點,之后休眠,線程二也進入了B點。所以在B點的時候也有可能是多個線程。緊接著 到達了 scnchronized塊中,因為同步塊里只允許一個線程,所以當線程1運行完了之后,線程2才進入,如果用下面的錯誤寫法,會導致線程2進入的時候也會實例化一個對象,這就和我們要求的目的不一樣了,所以還要進行一個null判斷。
關鍵詞volatile起到的作用?
答:
1.首先應該明確關鍵詞volitile在java中的作用

可見性要更加復雜一些,它必須確保釋放鎖之前對共享數據做出的更改對于隨后獲得該鎖的另一個線程是可見的 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值

用volatile來包裝變量來實現對多個線程的可見性,即線程1修改了變量,線程2中的變量值隨之也發送變化。如果不使用volatile,當線程1執行完畢之后,線程2進入C點,因為沒有使用volatile所以,這時候線程2中instance還有可能為null

2.禁止重排優化,關于這點可以參考這篇博客,寫的非常好。
注意 這種單例模式的實現方式,并不是一個優秀的方式,太復雜了

  • 內部類(holder class)
class Singleton{
    private static class SingletonHolder{
        static final Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

關于內部類實現單例模式,有以下幾點需要注意的
內部類 方式是如何實現懶加載?
答: 當它真正被用到的時候之前,即調用getInstance(), static class 不會被VM 加載,其實不光 static class ,任何的 Class 都不會被加載。參考 See the JLS -12.4.1 When Initialization Occurs
內部類方式是如何做到線程安全的?
答: 第一個線程調用 getInstance()的時候,JVM 會保持這個內部類,即singtonholder ,當第二個線程也在同時調用getInstance(),JVM 會等到第一個線程完成加載之后才會讓給第二個線程訪問。所以第二個線程得到的是已經加載完成的singletonholder。并且 JLS 規則保證每個類只會被第一次用到的時候加載一次。
參考 singleton-pattern-bill-pughs-solution

  • 枚舉(enum)
public enum Sington {
// 只會有一個INSTANCE 實例
    INSTANCE;
    public void foo() {
        //to do someing
    }
}
//調用方式,在下面這樣訪問的情況下才會加載。
Sington.INSTANCE.foo();

其實enum的實現方式,enum里面的字段屬性為 public static final,另外enum方式其實算偽懶加載吧,應該放在餓漢模式里面的。至于原因,我下面將會做出解釋。

關于餓漢和懶漢的思考

在說到這點的時候,首先你確認你知道類是在什么時候加載的么?

當類第一次被用到的時候,才會被JVM加載,并且初始化所有static 變量。注意這里是說的是加載,并不是實例化。
Object object = new Object(),類似于這樣叫實例化

正如下面這個例子。

public class LazyEnumTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Sleeping for 5 seconds...");
        Thread.sleep(5000);
        System.out.println("Accessing enum...");
        LazySingleton lazy = LazySingleton.INSTANCE;
        System.out.println("Done.");
    }
}
//測試一
enum LazySingleton {
    INSTANCE;
    static { System.out.println("Static Initializer"); }
}
// 測試二
public class Singleton {
  //private static final String testStr = "test";
 //private static final int testNum = 0;
 //private static final Object testObj = new Object();
    private static final Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
    
   public static void foo(){
    //to do some thing
  }
}

下面是運行結果

$ java LazyEnumTest
Sleeping for 5 seconds...
Accessing enum...
Static InitializerDone.

所以只有在訪問 LazySingleton.INSTANCE才會被加載,所以Enum也會被當作懶加載。
到了這個時候你也許會說,那測試二,即餓漢模式豈不是也是懶加載了。對的,你說的沒錯,的確是懶加載。即使你進行下面的操作,它也是懶加載的。

public class LazyEnumTest {
//此時,并未Singleton類并未加載
    private static final Singleton singleton= null;
    public static void main(String[] args) throws InterruptedException {
//在這個時候Singleton類才加載
      singleton = Singleton.getInstance();
    }
}

所以餓漢模式和懶漢模式其實差不多,對吧,都是懶加載了,那為什么還需要懶漢模式那。
區別在上面我舉的例子上。因為作為一個單例模式,你可能依賴一下其他的東西,才能在 private Singleton() {} 完成初始化,比如你需要上面的testObj

當你在一個地方使用如下的寫法

//這個時候,類就要進行加載。這時候,我只是要打印一下這個testObj,
//但是這個類就已經加載。這并不是我期望的效果,這個時候就該使用懶漢模式的
System.out.println(Singleton.testObj);
// 或者調用foo()也會一樣的效果
Sington.foo();
// 例外的情況,下面的調用,并不會引起類的加載
System.out.println(Singleton.testStr);
System.out.println(Singleton.testNum)

上面例外的原因如下

The use or assignment of a static field declared by a class or interface, except for static fields that are final and initialized by a compile-time constant expression (in bytecodes, the execution of a getstatic or putstatic instruction)
ps ---- it only applies to "static fields that are final and initialized by a compile-time constant expression":

大意就是,當 類或者接口里面的變量屬性,為 staic final 時候,并且會被 編譯期 常量初始化,該屬性才會被加載。但這種操作不會引起類的加載

private static final String testStr = "test";  //compile time constant
 private static final int testNum = 0; //compile time constant
private static final Object testObj = new Object(); //initialised at runtime

參考 Singleton via enum way is lazy initialized?

總結

所以以后使用 單例模式,最好的應該 內部類的方式,但如果你進行初始化的時候,不需要依賴一些其他的東西,那用Enum方式是最好的選擇,否則就是內部類的方式。此外Enum還保證了序列化的時候也只產生一個實例。

至于 單例模式和序列化就是另一個故事額。
好累,不想講了。想了解的請點擊這里。。。。單例模式和序列化

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容