劍指Offer題目2:實現 Singleton 模式(Java)

p32 面試題2:實現Singleton 模式面試

一、有缺陷的 Double Check 單例

/**
 * 多線程高效單例 Double Check
 * @Author: hebin.yang
 * @CreateDate: 2019-06-30 11:35
 */
public class Ex2Singleton3 {

    private static Ex2Singleton3 instance;

    private Ex2Singleton3() {}

    /**
     * Double Check
     * 分析:因爲 synchronized 關鍵字能夠確保在釋放鎖以前,將數據刷新到主內存中,線程2 能夠讀取到最新的值
     * 從而確保不會重複建立對象。
     * 缺陷:無序性
     * new 一個對象分兩步,並且是無序的。
     * @return
     */
    public static Ex2Singleton3 getInstance() {
        if (instance == null) {
            synchronized (Ex2Singleton3.class) {
                if (instance == null) {
                    instance = new Ex2Singleton3();
                }
            }
        }
        return instance;
    }
}
複製代碼

二、安全版 Double Check 單例

/**
 * 安全版 Double Check 單例模式
 * @Author: hebin.yang
 * @CreateDate: 2019-06-30 16:02
 */
public class Ex2Singleton4 {

    /**
     * 增長 volatile 修飾,禁止建立對象指令重排序,修復多線程下因爲指令重排序致使的異常
     */
    private static volatile Ex2Singleton4 instance;
    
    private Ex2Singleton4() {}
    
    public static Ex2Singleton4 getInstance() {
        if (instance == null) {
            synchronized (Ex2Singleton4.class) {
                if (instance ==  null) {
                    instance = new Ex2Singleton4();
                }
            }
        }
        return instance;
    }
}
複製代碼

關於 volatile 指令重排序的分析,詳見如下文章:安全

Double check 爲什麼須要 volatile?bash

劍指Offer 說 double check 方法有點複雜容易出錯實在有點牽強。多線程

三、利用靜態內部類初始化單例

特性:利用在 Java 中,靜態內部類只會在第一次調用到時進行初始化的特性,實現單例模式。post

/**
 * 靜態內部類單例模式
 * @Author: hebin.yang
 * @CreateDate: 2019-06-30 16:33
 */
public class Ex2Singleton5 {

    static class SingletonHolder {
        private static Ex2Singleton5 INSTANCE = new Ex2Singleton5();
    }

    private Ex2Singleton5() {}

    public static Ex2Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
複製代碼

分析:當Ex2Singleton5 加載時,並不會實例化靜態內部類,只有當第一次調用靜態內部類的 INSTANCE 時,纔會加載 SingletonHolder,從而實例化 Ex2Singleton5 這個單例的對象。性能

優點:因爲這種場景下單例對象的實例化,是在完成靜態內部類加載時完成的,因此天生對多線程友好。getInstance() 也不須要關鍵字來同步。ui

優勢:延遲加載,無需增長同步關鍵字(減小性能損耗)。spa

靜態內部類又是如何實現線程安全的呢?

原理線程

因爲靜態內部類加載時只會執行一次類初始化方法 <clinit>(),JVM 對該方法加了鎖,確保當多個線程同時試圖去執行類的初始化方法 <clinit>()時,只有一個線程能執行,其他線程阻塞。而且,確保該方法只被執行一次。code

因此實現了線程安全。

總結:一個靜態內部類被加載初始化時,從類加載器的角度來看,是線程安全的,而單例對象 INSTANCE 在這個靜態內部類中是一個靜態 Field,因此它跟隨這個靜態內部類的加載而初始化,且只在類加載時初始化一次。

詳細分析可參考:《類加載時機彙總》

相關文章
相關標籤/搜索