前几天有个刚工作的读者问我:单例模式到底该怎么学,说网上的博客看了一堆,饿汉、懒汉、双重检查锁、静态内部类、枚举,抄来抄去,代码都能默写了,可面试官一句“JDK 源码里哪里用了单例”,人直接愣住。这个问题我太熟了。单例模式可能是所有设计模式里代码量最小、但坑最多、最能把人从“背写法”拉到“看本质”的一个模式。我自己当年把 java.lang.Runtime 的源码翻出来一行行读完,很多过去想不通的东西瞬间就通了。这篇博文,就顺着 JDK 源码把单例模式重新拆一遍,不背结论,只看代码。
1. 为什么单例模式非要拿 JDK 源码来学
1.1 网上教程和 JDK 源码之间的差距
大多数单例模式教程会给你一张五种写法的对照表:饿汉式、懒汉式、双重检查锁、静态内部类、枚举,然后告诉你“推荐用枚举”。表格背得滚瓜烂熟,但问到“为什么 JDK 的 Runtime 类要用饿汉式,而 Spring 的 Bean 却用容器缓存”,很多同学就答不上来了。
这不能怪学习者,因为网上教程普遍缺少一个最重要的东西:真实工程里的取舍逻辑。JDK 源码恰恰是最权威的“真实工程”。它是 Oracle 和 OpenJDK 社区工程师多年沉淀下来的代码,里面每个类的写法都有明确理由,不是为了演示设计模式而设计的。你能看到 Runtime 这种标准的饿汉式,也能看到 Collections 里用不可变单例思想实现的空集合,甚至能看到反射机制对枚举单例的“天然保护”。这些才是单例模式在现实世界中的真实形态。
1.2 读源码前要有的三个底层概念
看 JDK 源码不是逐行读注释,而是带着底层机制去看。读单例相关源码之前,我建议你先想清楚三件事。
第一,类加载机制。一个类在同一个类加载器下只会被加载一次,静态字段的初始化发生在类加载的初始化阶段,这个阶段由 JVM 保证线程安全。饿汉式和静态内部类单例之所以不需要加锁,靠的就是这一点。第二,对象创建的额外通道。除了 new,反射可以调用私有构造器,反序列化可以不经过构造器直接重建对象,这些都会破坏单例。JDK 源码里对枚举单例的保护,就是在这些通道上做文章。第三,字节码视角。如果你只会看 .java 文件,很多细节会漏掉。比如双重检查锁为什么要 volatile,你去看字节码和 JMM 相关源码才能彻底理解。
有了这三个概念,再去啃 Runtime、Enum、Constructor、ObjectInputStream 这些类的源码,你会发现自己不是在“读设计模式”,而是在读 Java 平台的设计哲学。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Runtime 类源码:JDK 里最标准的饿汉式长什么样
2.1 Runtime 源码里的单例骨架
java.lang.Runtime 是 JDK 中教科书般的饿汉式单例。我以 JDK 8 的源码为例,去掉业务方法,单例骨架大概长这样:
java复制public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {
}
}
就这么几行,但每一个细节都有讲究。
构造器是 private,外部完全没办法 new 一个 Runtime 出来。静态字段 currentRuntime 在类加载阶段就被初始化,也就是 JVM 保证类加载只发生一次,所以这个实例在 JVM 生命周期内全局唯一。getRuntime() 是静态方法,所有调用方都拿到同
