單例設計模式 劍指offer

單例設計模式

1.單例模式的定義

單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

2.單例模式的特點

  • 單例類只能有一個實例。
  • 單例類必須自己創建自己的唯一實例。
  • 單例類必須給所有其他對象提供這一實例。

3.單例模式的應用

  • 在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。
  • 這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了避免不一致狀態。

4.單例模式的Java代碼

單例模式分爲懶漢式(需要纔去創建對象)和餓漢式(創建類的實例時就去創建對象)。

5.餓漢式

  • 屬性實例化對象
//餓漢模式:線程安全,耗費資源。
public class HugerSingletonTest {
    //該對象的引用不可修改
    private static final HugerSingletonTest ourInstance = new HugerSingletonTest();

    public static HugerSingletonTest getInstance() {
        return ourInstance;
    }

    private HugerSingletonTest() {}
}

  • 在靜態代碼塊實例對象

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

    public static Singleton getInstance() {
        return ourInstance;
    }

    private Singleton() {}
}

分析:餓漢式單例模式只要調用了該類,就會實例化一個對象,但有時我們並只需要調用該類中的一個方法,而不需要實例化一個對象,所以餓漢式是比較消耗資源的。

6.懶漢式

  • 非線程安全
public class Singleton {
    private static Singleton ourInstance;

    public static Singleton getInstance() {
        if (null == ourInstance) {
            ourInstance = new Singleton();
        }
        return ourInstance;
    }

    private Singleton() {}
}

分析:如果有兩個線程同時調用getInstance()方法,則會創建兩個實例化對象。所以是非線程安全的。

  • 線程安全:給方法加鎖
public class Singleton {
    private static Singleton ourInstance;

    public synchronized static Singleton getInstance() {
        if (null == ourInstance) {
            ourInstance = new Singleton();
        }
        return ourInstance;
    }

    private Singleton() {}
}

分析:如果有多個線程調用getInstance()方法,當一個線程獲取該方法,而其它線程必須等待,消耗資源。

  • 線程安全:雙重檢查鎖(同步代碼塊)
public class Singleton {
    private static Singleton ourInstance;

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

    private Singleton() {}
}

  

分析:爲什麼需要雙重檢查鎖呢?因爲第一次檢查是確保之前是一個空對象,而非空對象就不需要同步了,空對象的線程然後進入同步代碼塊,如果不加第二次空對象檢查,兩個線程同時獲取同步代碼塊,一個線程進入同步代碼塊,另一個線程就會等待,而這兩個線程就會創建兩個實例化對象,所以需要在線程進入同步代碼塊後再次進行空對象檢查,才能確保只創建一個實例化對象。

  • 線程安全:靜態內部類
public class Singleton {
    private static class SingletonHodler {
        private static final Singleton ourInstance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHodler.ourInstance;
    }

    private Singleton() {}
}

分析:利用靜態內部類,某個線程在調用該方法時會創建一個實例化對象。

  • 線程安全:枚舉
enum SingletonTest {  
    INSTANCE;  
    public void whateverMethod() {
        
    }
}

分析:枚舉的方式是《Effective Java》書中提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,但是在枚舉中的其他任何方法的線程安全由程序員自己負責。還有防止上面的通過反射機制調用私用構造器。不過,由於Java1.5中才加入enum特性,所以使用的人並不多。

  • 線程安全:使用ThreadLocal
public class Singleton {
    private static final ThreadLocal<Singleton> tlSingleton =
            new ThreadLocal<Singleton>() {
                @Override
                protected Singleton initialValue() {
                    return new Singleton();
                }
            };

    public static Singleton getInstance() {
        return tlSingleton.get();
    }
    
    private Singleton() {}
}

分析:ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

  • 線程安全:CAS鎖
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    /**
     * 用CAS確保線程安全
     */
    public static Singleton getInstance() {
        while (true) {
            Singleton current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

    private Singleton() {}
}

7.指令重排序

  • 我們再來思考一個問題,就是懶漢式的雙重檢查版本的單例模式,它一定是線程安全的嗎?我會毫不猶豫的告訴你—不一定,因爲在JVM的編譯過程中會存在指令重排序的問題。
  • 其實創建一個對象,往往包含三個過程。
    對於singleton = new Singleton(),這不是一個原子操作,在 JVM 中包含的三個過程。
  • 1>給 singleton 分配內存
  • 2>調用 Singleton 的構造函數來初始化成員變量,形成實例
  • 3>將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)
  • 但是,由於JVM會進行指令重排序,所以上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,則在 3 執行完畢、2 未執行之前,被l另一個線程搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以這個線程會直接返回 instance,然後使用,那肯定就會報錯了。
  • 針對這種情況,我們有什麼解決方法呢?那就是把singleton聲明成 volatile ,改進後的懶漢式線程安全(雙重檢查鎖)的代碼如下:
public class Singleton {
    //volatile的作用是:保證可見性、禁止指令重排序,但不能保證原子性
    private volatile static Singleton ourInstance;

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

    private Singleton() {
    }
}

8.單例模式在JDK8源碼中的使用

當然JDK源碼中使用了大量的設計模式,那哪些地方使用了單例設計模式呢?

  • Runtime類部分源碼如下
//餓漢式單例設計模式
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {
    }
    
    //省略很多行
}

參考鏈接

深入淺出單實例Singleton設計模式

交流學習

我的博客
學習交流或獲取更多資料歡迎加入QQ羣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章