為什么大廠面試官喜歡問單例模式

單例模式的五種實(shí)現(xiàn)看似喪心病狂,其實(shí)是一個由淺入深,再到化繁為簡的過程。

餓漢式

餓漢式的實(shí)現(xiàn)思路是初始化的過程中就加載完成單例,而不是延遲再加載,有一種饑不擇食的感覺,代碼如下。

import java.util.concurrent.atomic.AtomicLong;

public class IdGen {
    private AtomicLong id = new AtomicLong(0);
    private static final IdGen instance = new IdGen();

    private IdGen() {
    }

    public static IdGen getInstance() {
        return instance;
    }

    public long getId() {
        var r = id.incrementAndGet();
        System.out.println(r);
        return r;

    }
}

這就適合一些業(yè)務(wù)加載時間長或者必須提前加載的使用場景。不少人覺得業(yè)務(wù)加載時間長應(yīng)該延遲加載,這樣才能加快啟動速度。其實(shí)延遲加載會影響用戶體驗,因為延遲加載后,用戶請求發(fā)過來,服務(wù)器還沒加載完一直卡著響應(yīng),體驗極差。

注意看,Runtime類中的單例模式是用餓漢式實(shí)現(xiàn)的。

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author unascribed
 * @see java.lang.Runtime#getRuntime()
 * @since JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {
    }

    // ....
    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }
    // ...
}

懶漢式

最原始的懶漢式單例給方法加鎖,實(shí)現(xiàn)延遲加載。但這種方法根本就不支持并發(fā),是個串行操作。如果你用這種方法實(shí)現(xiàn),一旦遇到并發(fā)的資源訪問,系統(tǒng)根本hold不住,背鍋概率99%。

import java.util.concurrent.atomic.AtomicLong;

public class IdGen {
    private AtomicLong id = new AtomicLong(0);
    private static IdGen instance;

    private IdGen() {
    }

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

    public long getId() {
        var r = id.incrementAndGet();
        return r;

    }
}

雙重檢測

雙重檢測既實(shí)現(xiàn)了延遲加載,又支持了并發(fā)。不少人可能會疑惑,為什么要檢測兩次instance==null,原因其實(shí)很簡單,如果只檢測一次,假設(shè)兩個線程調(diào)用getInstance(),其中一個線程new出了單例,另一個線程等待第一個釋放鎖之后仍然可以new出單例,那這就破壞了單例模式,因此需要檢測兩次。

有的人可能會說創(chuàng)建對象singleton = new Singleton()的底層是分為三個步驟:

  1. 為對象分配內(nèi)存空間;
  2. 初始化對象;
  3. 對象地址的引用。

因此要給 instance 成員變量加上 volatile 關(guān)鍵字,禁止指令重排序才行。實(shí)際上,只有很低版本的 Java 才會有這個問題。我們現(xiàn)在用的高版本的 Java 已經(jīng)在 JDK 內(nèi)部實(shí)現(xiàn)中解決了這個問題(解決的方法很簡單,只要把對象 new 操作和初始化操作設(shè)計為原子操作,就自然能禁止重排序)。

import java.util.concurrent.atomic.AtomicLong;

public class IdGen {
    private AtomicLong id = new AtomicLong(0);
    private static IdGen instance;

    private IdGen() {
    }

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

    public long getId() {
        var r = id.incrementAndGet();
        return r;
    }
}

內(nèi)部靜態(tài)類

內(nèi)部靜態(tài)類的實(shí)現(xiàn)也實(shí)現(xiàn)了延遲加載,把并發(fā)的操作交給了JVM來處理,不過仍然有序列化和反射破壞單例模式的問題。

import java.util.concurrent.atomic.AtomicLong;

public class IdGen {
    private AtomicLong id = new AtomicLong(0);

    private IdGen() {
    }

    private static class SingletonHolder {
        private static final IdGen instance = new IdGen();
    }

    public static IdGen getInstance() {
        return SingletonHolder.instance;
    }

    public long getId() {
        var r = id.incrementAndGet();
        return r;
    }
}

枚舉

枚舉也是實(shí)現(xiàn)了延遲加載,并發(fā)交由JVM處理,并且沒有序列化和破壞單例模式的問題,可以說是延遲加載中的最優(yōu)解。

import java.util.concurrent.atomic.AtomicLong;

// 測試類直接調(diào)用IdGen.INSTANCE
public enum IdGen {
    INSTANCE;

    private AtomicLong id = new AtomicLong(0);

    public long getId() {
        var r = id.incrementAndGet();
        System.out.println(r);
        return r;
    }
}

總結(jié)

綜上,我們不難發(fā)現(xiàn)單例大體可以分為提前加載和延遲加載兩大類,餓漢式單例在指定的應(yīng)用場景是無可替代的,枚舉實(shí)現(xiàn)的單例簡潔安全,雙重檢測能考察你對鎖的理解,因此單例面試題深得面試官喜愛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容