1. 程式人生 > >單例設計模式(這一篇足夠了)

單例設計模式(這一篇足夠了)

知識 執行 cpu new stat 禁止 初始 設計 內存空間

  單例模式真是一個老掉牙的問題了,不過我今天是要說些裏面更深點的知識,閑話少說,直接來代碼

  1、餓漢式

   相信這種寫法大家都知道,一開始接觸單例的時候,大家應該都是用的這種方法:

package com.hd.single;

public class Singleton {

    private Singleton(){}
    private static Singleton instance = new Singleton();

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

  這種方式優點就是線程安全, 缺點也很明顯,就是類加載的時候,就已實例化該對象了,後面有可能用不到這個實例對象,這樣就會造成空間浪費。因此就有了懶加載方式。

  2、懶漢式

  1)懶漢式L1

package com.hd.single;

public class Singleton2 {

    private Singleton2(){}
    private static Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null)        //1
            instance 
= new Singleton2();  //2 return instance; } }

  這種懶漢式的優點和缺點也很明顯,優點是按需加載,節省空間, 缺點是線程不安全。簡單說就是,有可能線程A執行到“1”處時,阻塞住了,線程B搶到CPU,進來執行並實例化對象,然後線程A醒來後,繼續往下執行,這樣線程A和B取到的就是不同的對象。因此,又有了線程安全的版本。

package com.hd.single;

public class Singleton2 {

    private Singleton2(){}
    private static Singleton2 instance;

    
public static synchronized Singleton2 getInstance(){ if(instance == null) instance = new Singleton2(); return instance; } }

  但是加了synchronized 之後會造成線程阻塞,影響性能。於是又提出了雙檢鎖的方式

 1 package com.hd.single;
 2 
 3 public class Singleton2 {
 4 
 5     private Singleton2(){}
 6     private static Singleton2 instance;
 7 
 8     public static Singleton2 getInstance(){
 9         if(instance == null){
10             synchronized (Singleton2.class){
11                 if(instance == null){
12                     instance = new Singleton2();
13                 }
14             }
15         }
16         return instance;
17     }
18 }

  看似雙檢鎖的方式很完美,既解決了線程安全的問題,又兼顧了性能問題: 線程先判斷instance變量是否為空,如果不為空,則直接返回。否則進入同步塊去實例化對象。但事實這是一個錯誤的優化!

  重點就是第12行代碼(instance = new Singleton2();), 它創建了一個對象。這一行代碼可以分解為如下的3行代碼:

memory = allocate();         //1:分配對象的內存空間
ctorInstance(memory);        //2:初始化對象
instance = memory;           //3:設置instance指向剛分配的內存地址

  上面2和3這兩步之間,有可能會被重排序,2和3重排序之後的執行時序如下:

memory = allocate();         //1:分配對象的內存空間
instance = memory;           //3:設置instance指向剛分配的內存地址
                           //註意此時對象還沒有被初始化
ctorInstance(memory);        //2:初始化對象
                                    

  因此如果有線程A執行到3時,此時instance變量確實不為空,然後線程B判斷instance不為空後返回,那麽這是線程B 取到的就是一個空的對象。顯示這樣是有問題的,因此為了防止出現這個問題,需要使用volatile變量,來禁止指令重排序。

  2)懶漢式 L2(基於volatile的解決方案)

package com.hd.single;

public class Singleton {

    private Singleton(){}
    private volatile static Singleton instance;

    public static Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

  我們除了通過volatile的方式來禁止指令重排序,還可以提供另外一種思路:允許2和3重排序,但不允許其它線程“看到”這個重排序。 前面正是因為線程B看到了重排序,instance變量不為空,所以才造成其取到空的對象。

  3)懶漢式L3(基於靜態內部類的方案)

package com.hd.single;
public class LazySingleton2 {
    private LazySingleton2() {
    }
    static class SingletonHolder {
        private static final LazySingleton2 instance = new LazySingleton2();
    }
    public static LazySingleton2 getInstance() {
        return SingletonHolder.instance;
    }
}

  因為 在加載外部類時,其內部類不會同時被加載。只有調用 getInstance方法的時候,內部類才會去被加載,且只加載一次,不存在並發問題,因此是線程安全的。

  另外,在getInstance()方法中沒有使用synchronized關鍵字,因此沒有造成多余的性能損耗。

  本文給出了多個版本的單例模式,供我們在項目中使用。一般用L2,L3就基本夠用。

單例設計模式(這一篇足夠了)