兄弟们,咱们今天聊个看着最简单,但其实全是“雷”的面试题——手写单例模式。

很多人觉得这题是送分题,但是这道题不仅考代码,更考你对 并发编程(JMM) 和 JVM 类加载机制 的理解。代码写对了只是及格,要能把背后的“为什么”讲清楚。

咱们今天就把最常见的三种写法扒得干干净净。


1. 标准答案:双重检查锁(DCL)—— 也是最容易翻车的写法 

如果面试官让你“手写一个线程安全的懒汉式单例”,别犹豫,直接上 Double Check Lock (DCL)。这是行业标准答案。

但是,90% 的人挂就挂在少写了一个关键字。

public class Singleton {

    // 🛑 重点:必须加 volatile!没加这行等于没写!
    private static volatile Singleton instance;

    // 1. 构造器私有化,堵死外部 new 的路
    private Singleton() {}

    public static Singleton getInstance() {
        // 2. 第一层检查:如果已经有对象了,就别去抢锁了,提升性能
        if (instance == null) {
            synchronized (Singleton.class) {
                // 3. 第二层检查:防止刚才排队进来的线程重复创建
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

 死亡追问:为什么要加 volatile?

如果你代码写完了,面试官大概率会指着 volatile 问你:“这玩意儿能删吗?”

这时候千万别虚,你要告诉他:绝对不能删,因为要防止“指令重排序”。

instance = new Singleton(); 这行代码看着是一句话,但在 JVM 的微观世界里,它其实分了三步走:

  1. 分配内存 (memory = allocate())

  2. 初始化对象 (ctorInstance(memory))

  3. 引用赋值 (instance = memory) —— 这步执行完,instance 就不再是 null 了

问题出在哪?

编译器和 CPU 为了跑得快,可能会自作聪明地把顺序改成 1 -> 3 -> 2。

翻车场景:

  1. 线程 A 执行了 1 和 3(分配了内存,赋了值),但还没来得及执行 2(还没初始化,对象是个半成品)。

  2. 这时候 线程 B 进来了,在“第一层检查”时发现 instance != null(因为它已经被赋值了)。

  3. 线程 B 直接拿着这个半成品对象去用了。

  4. 空指针异常或者数据错乱。

volatile 的作用: 就是加了一道内存屏障,强行告诉 CPU:“兄弟,老老实实按 1 -> 2 -> 3 的顺序干活,别乱优化。”


2. 优雅装X写法:静态内部类 

如果面试官看你 DCL 写得挺溜,可能会问:“那一堆锁看着挺烦的,有没有不用锁还线程安全的懒汉式写法?”

这时候,你可以微微一笑,祭出静态内部类。

public class Singleton {
    private Singleton() {}

    // 静态内部类:只有在被调用时才会被加载
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

为什么这玩意儿稳?

它利用了 Java 的类加载机制。SingletonHolder 这个内部类,只有在 getInstance() 被调用的时候才会被 JVM 加载。

而且,JVM 在加载类的过程中是天然线程安全的。这属于是借了 JVM 的力,省了自己的锁。既实现了懒加载,又极其简洁。


3. 朴实无华写法:饿汉式 

“饿汉”的意思就是:我很饿,不管你吃不吃,我先把饭做好端上来。

public class Singleton {
    // 类加载的时候就创建好了,绝不拖泥带水
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

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

很多新手不敢写这个,怕被面试官觉得太简单。其实不然。

它的底气在于:

这也是靠 JVM 的类加载机制(<clinit> 方法)保证线程安全的。在任何线程访问它之前,它就已经躺在内存里了,绝对没有并发问题。


4. 终极博弈:面试怎么选?

面试官最后可能会问:“这几种写法,生产环境你选哪个?”

别只说一个,要学会分类讨论(这招显着你很专业):

  • 选饿汉式 (Eager):如果这个单例对象不大,或者系统启动后肯定会用到。

    • 理由:简单粗暴,没有锁的性能损耗,启动时就搞定了,运行时速度最快。

  • 选懒汉式 (DCL / 静态内部类):如果这个对象非常重(比如初始化连接池要很久),或者不一定会被用到。

    • 理由:启动速度快,以时间换空间,避免内存浪费。

总结一下

  1. 面试手写:首选 DCL(双重检查锁),因为能顺带秀一波 volatile 和内存模型知识点。

  2. 日常开发:首选 静态内部类 或 枚举(枚举还没讲,那是另一个防反射的故事了),因为写着省心,不容易出错。

  3. 高性能要求:如果内存不是瓶颈,饿汉式 其实是最快的。

好了,下次面试再让你写单例,记得先把 volatile 写上,然后坐等面试官投来赞许的目光

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐