面试必考:手写单例模式还能翻车?这几个坑你填上了吗?
兄弟们,咱们今天聊个看着最简单,但其实全是“雷”的面试题——手写单例模式。
很多人觉得这题是送分题,但是这道题不仅考代码,更考你对 并发编程(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 的微观世界里,它其实分了三步走:
-
分配内存 (
memory = allocate()) -
初始化对象 (
ctorInstance(memory)) -
引用赋值 (
instance = memory) —— 这步执行完,instance 就不再是 null 了
问题出在哪?
编译器和 CPU 为了跑得快,可能会自作聪明地把顺序改成 1 -> 3 -> 2。
翻车场景:
-
线程 A 执行了 1 和 3(分配了内存,赋了值),但还没来得及执行 2(还没初始化,对象是个半成品)。
-
这时候 线程 B 进来了,在“第一层检查”时发现
instance != null(因为它已经被赋值了)。 -
线程 B 直接拿着这个半成品对象去用了。
-
空指针异常或者数据错乱。
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 / 静态内部类):如果这个对象非常重(比如初始化连接池要很久),或者不一定会被用到。
-
理由:启动速度快,以时间换空间,避免内存浪费。
-
总结一下
-
面试手写:首选 DCL(双重检查锁),因为能顺带秀一波
volatile和内存模型知识点。 -
日常开发:首选 静态内部类 或 枚举(枚举还没讲,那是另一个防反射的故事了),因为写着省心,不容易出错。
-
高性能要求:如果内存不是瓶颈,饿汉式 其实是最快的。
好了,下次面试再让你写单例,记得先把 volatile 写上,然后坐等面试官投来赞许的目光

更多推荐
所有评论(0)