public class Kettle {
private boolean empty;
private boolean boiled;
public Kettle() {
empty = true;
boiled = false;
* 壶里没水,倒入冷水
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
* 壶里有水且水没在烧时,烧水
public void boil() {
if (!isEmpty() && !isBoiled()) {
boiled = true;
* 壶里有水且水烧好了,把水倒出来
public void drain() {
if (!isEmpty() && isBoiled()) {
empty = true;
public boolean isEmpty() {
return empty;
public boolean isBoiled() {
return boiled;
甲和乙都想用这个壶烧水,但是壶只有一个。在程序里模拟,甲和乙可以当作是不同的线程,要拿壶烧水就是要先拿到这个唯一的实例,再去烧水。
很显然,在我们这个类里,没有做任何保证实例唯一的实现。任何其它类都可以new出一个新的kettle实例来。
要怎么保证在任何情况下都只会有一个kettle实例呢?这就是单例模式所做的事情。
我们先来看一下它的定义,再看怎么去实现。
《Design Patterns》(《Head First Design Patterns》沿用此定义)中对单例模式的定义如下:
The Singleton Pattern ensures a class has only one instance, and provides a global point of access to it.
也就是说,单例模式的实现需要满足两个要求:
有且只有一个实例对象;
提供一个全局的访问接口。
要满足这两个需求,有一个经典的解法。我们的Kettle类对外提供一个公共接口(global access point),保证使用这个接口的用户能拿到这个唯一的实例对象(only one instance),并且再没有别的方法可以创建Kettle实例了。
公共接口(这里的接口指API而不是Java的Interface)的方法签名如下,getInstance
是一个在单例模式里约定俗成的方法名:
public static Kettle getInstance()
再加一个私有的静态成员变量:
private static Kettle instance;
要保证没有别的方法可以创建实例,可以将Kettle的构造函数设为私有,使其只能在类内调用:
private Kettle() {
/* 初始化成员变量 */
私有化构造函数也是,比如JDK里的Math类:
public final class Math {
* Don't let anyone instantiate this class.
private Math() {}
这就是实现单例模式的基础。
根据静态成员变量instance
是在第一次运行getInstance()
时赋值还是在类加载时赋值,可以将单例模式的实现大致分类两类,懒加载(懒汉式)和预加载(饿汉式)。
懒加载,是在程序需要时才实例化对象,不会占用不必要的空间。
预加载,是在类加载时实例化对象,有可能一直到程序结束都不会用到这个对象,白白地浪费了内存空间;在程序刚运行时就实例化好了,之后要用的时候就可以直接调用,在这里节省了时间。
双重检测锁模式(DCL)是典型的懒加载实现,枚举类(Enum)是典型的预加载实现。不同的实现适用于不同的使用场景。
应用场景:
数据库连接池
Spring的单例Bean
在以下模式中,多数情况下只会生成一个实例:
Abstract Factory
Builder
Facade
Prototype
懒加载的实现
懒加载(lazy initialization/loaded)的四种实现中,经典解法在多线程下无法保证唯一实例;加synchronized锁做到了线程安全,满足了单例模式的要求,但在性能上有不必要的浪费;双重检测锁模式满足了单例模式的要求,性能上比加synchronized锁有优化,是较好的实现;JVM确保了静态内部类的线程安全。
经典解法(线程不安全)
public class Kettle {
private static Kettle instance;
private Kettle() {
/* 初始化成员变量 */
public static Kettle getInstance() {
if (instance == null) {
instance = new Kettle;
return instance;
/* 成员变量和方法 */
经典解法在每次调用getInstance()
方法时校验一下instance
变量是否为空,为空则先给它赋值,然后向用户返回变量。
在多线程情况下很有可能拿到不同的实例。
虽然线程开小了不太能看到这种错误。这里开了一百万个线程,跑了几分钟,打印出来一看,set里还是只有一个对象:
Set<Kettle01> set = new HashSet<>();
for (int i = 0; i < 100_0000; i++) {
new Thread(() -> {
set.add(Kettle.getInstance());
}).start();
System.out.println(set.size());
可以来脑内模拟一下两个线程交替执行的特殊情况,可以看到,instance
变量先后被赋予了不同的值,显然违背了单例模式的要求:
线程1(甲)
线程2(乙)
instance
的值
这引入了一个直观的解法,我直接在getInstance()
方法上加把锁不久好了?
public class Kettle {
private static Kettle instance;
private Kettle() {
/* 初始化成员变量 */
// 在静态方法上加锁
public static synchronized Kettle getInstance() {
if (instance == null) {
instance = new Kettle();
return instance;
/* 成员变量和方法 */
加锁很好地解决了多线程下获取不同实例对象的问题,但是又引入了性能的浪费。
试想一下,在程序运行的初期,我们称其为阶段一时期,instance
还是空值,这时有两个线程想要来获取Kettle
的唯一实例,调用了getInstance()
方法,因为有加锁,先拿到锁的线程完成了实例的赋值,后来的线程拿到锁时instance
已经不为空了,直接返回instance
,这符合我们对程序的要求。
在此之后,我们称其为阶段二时期,instance
已经有值了,再有多个线程来调用getInstance()
方法时,依旧要挨个拿到锁来进入方法体,但实际上,可以直接返回instance
变量,不再需要排队以防初始化变量出错,因为已经给变量赋过值了,这时的加锁就是不必要的了。
由此引入双重检测锁模式。
双重检测锁模式(DLC)(perfect!)
双重检测锁模式就是只在变量需要初始化的时候再加锁。双重检测的意思就是两次判断变量是否为空,一次在加锁前,一次在加锁后。如果第一次检测的时候,发现变量为空,说明是在程序运行的初期,也就是阶段一时期,需要加锁来保证变量正确地初始化;如果发现变量不为空,说明这是在阶段二时期,不需要加锁了。
public class Kettle {
// volatile 禁止指令重排
private static volatile Kettle instance;
private boolean empty;
private boolean boiled;
public Kettle() {
empty = true;
boiled = false;
public static Kettle getInstance() {
if (instance == null) { // a
// 加锁
synchronized (Kettle.class) { // b
if (instance == null) { // c
instance = new Kettle(); // d
return instance;
/* 成员变量和方法 */
volatile
注意,这里的instance
变量上加了一个volatile
关键字,这是用来禁止指令重排序的。
instance = new Kettle()
这条语句在Java里不是原子性的,它会分成三个指令:①在内存里划分一块要放变量的空间、②在这块空间上初始化一个Kettle实例、③变量/指针instance
指向这块空间。
在编译执行时,如果没有特殊声明,这三条语句会被编译器在确保单线程下运行结果最后正确的前提下,更改这三条指令的运行顺序。也就是说,在多线程下,可能发生这种情况:线程1发现instance
为null,加锁,执行instance = new Kettle()
语句的时候,三条指令被重排序了;先分配了一块内存空间,里头的值被初始化为默认值,然后将instance
指向这块空间,instance
就不是null了;这时候线程2过来,getInstance()
方法发现instance != null
,于是返回了尚未完全初始化、成员变量都是默认值的instance
,客户用了这个instance
,发生错误。
volatile
的含义是内存屏障(memory barrier),确保变量被写入之前的所有其它写操作都发生在这个操作之前,放在这里,就是③instance
变量被赋值这个操作发生之前,②初始化Kettle实例到这块内存空间上、初始化成员变量的操作必然已经执行完毕了,也就是禁止了指令重排序,不会再发生上面提到的错误。
线程1(甲)
线程2(乙)
instance
的值
构造函数虽然是私有的,但我们通过反射,很容易就能获取到它:
Constructor<?> constructor = Kettle.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object a = constructor.newInstance();
Object b = constructor.newInstance();
System.out.println(a == b);
打印出来是false
,显然不满足单例模式的要求啦。
虽然也可以在构造函数里再做一些处理,比如再判断一下变量是不是空、加一个flag
来判断变量是否已经初始化之类的,不过也可以通过反射轻松破解:
Constructor<?> constructor = Kettle.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object a = constructor.newInstance();
// 把instance变量再次置空,就可以继续初始化了
Field field = Kettle.class.getDeclaredField("instance");
field.setAccessible(true);
field.set(a, null);
Object b = constructor.newInstance();
System.out.println(a == b);
序列化也可以轻松破坏单例模式。readObject
会创建新的对象。
Kettle a = Kettle.INSTANCE;
FileOutputStream fos = new FileOutputStream("Kettle.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(a);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Kettle.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Kettle b = (Kettle) ois.readObject();
ois.close();
System.out.println(a == b);
得到的输出是false
。
怎么解决呢?加一个readResolve()
方法:
public class Kettle {
private static volatile Kettle instance;
public Kettle() {
/* 初始化成员变量 */
public static Kettle getInstance() {
if (instance == null) {
synchronized (Kettle.class) {
if (instance == null) {
instance = new Kettle();
return instance;
// 解决序列化问题
private Object readResolve() {
return instance;
/* 成员变量和方法 */
再次运行测试程序,得到的结果就是true
了。
查看源码,调用链如下:
package java.io;
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants {
public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException {
Object obj = readObject0(type, false);
return obj;
readObject0()
方法里,如果是object,会调用readOrdinaryObject()
方法:
private Object readObject0(Class<?> type, boolean unshared) throws IOException { case TC_OBJECT: ... return checkResolve(readOrdinaryObject(unshared));}
readOrdinaryObject()
方法里,有对是否有readResolve()
方法的判定,hasReadResolveMethod()
方法:
/* java.io.ObjectInputStream.java */
private Object readOrdinaryObject(boolean unshared)
throws IOException
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) // 是否有`readSolve()`方法
Object rep = desc.invokeReadResolve(obj); // 有的话,直接调用该方法
hasReadResolveMethod()
则是在ObjectStreamClass
类中,在构造函数里,通过反射获取这个方法:
package java.io;
public class ObjectStreamClass implements Serializable {
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
private Method readResolveMethod;
// 构造函数
private ObjectStreamClass(final Class<?> cl) {
if (serializable) {
// 通过反射获取
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
也就是说,测试程序里的ois.readObject()
最后调用了我们在Kettle类里写的readResolve()
方法,返回instance
变量。
静态内部类
静态内部类(Initialization-on-demand holder idiom)的实现是将instance
变量转交给静态内部类。Kettle
类被加载和初始化时,并不会初始化它的静态内部类LazyHolder
。只有在JVM确定LazyHolder
要执行的时候,才会初始化它。而只有在getInstance()
方法被调用时,LazyHolder
类才会被加载并初始化。Java语言特性确保了类的初始化是线程安全的。
public class Kettle {
private Kettle(){
/* 初始化成员变量 */
private static class LazyHolder {
static Kettle instance = new Kettle();
public static Kettle getInstance() {
return LazyHolder.instance;
/* 成员变量和方法 */
这里,反射和序列化的情况与DCL一致。
预加载的实现
预加载的两种实现都是线程安全的,因为是在类加载时赋值,利用Java语言特性,JVM帮助我们实现了多线程下的安全赋值。
在类加载时为静态变量赋值,JVM保证了多线程安全。
public class Kettle {
private static final Kettle instance = new Kettle();
private Kettle() {
/* 初始化成员变量 */
public static Kettle getInstance() {
return instance;
/* 成员变量和方法 */
JDK里有用到这种方法,比如RunTime类:
package java.lang;
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() {}
这里,反射和序列化的情况与DCL一致。
枚举类(enum)
public enum Kettle { INSTANCE; private boolean empty; private boolean boiled; Kettle() { curSize = 0; empty = true; boiled = false; } /* 成员变量和方法 */}
枚举类是怎么在类加载时就初始化实例的呢?因为enum
的实现机制,它的枚举变量都是在枚举类加载时初始化赋值的。可以从反编译的代码中一窥究竟。
Java自带的反编译工具是javap
。自行编译一下, javac Kettle.java -encoding utf-8
,得到.class
文件。再进行反编译,javap Kettle
:
λ javap KettleCompiled from "Kettle.java"public final class Kettle extends java.lang.Enum<Kettle> { public static final Kettle INSTANCE; public static Kettle[] values(); public static Kettle valueOf(java.lang.String); public void fill(); public void boil(); public void drain(); public boolean isEmpty(); public boolean isBoiled(); static {};}
可以看到,变量INSTANCE
是类Kettle的静态变量。那它是怎么初始化的呢?这里看不到,我们换个工具。
搜索Java反编译,看前面的搜索结果,有一个jd-gui-windows-1.6.6
的,我们用它打开一下Kettle.class
:
public enum Kettle { INSTANCE; private boolean boiled; private boolean empty; Kettle() { this.empty = true; this.boiled = false; } /* 其它方法 */}
跟我们源代码一样,换一个工具jad
,虽然老但有用:
λ jad KettleParsing Kettle... Generating Kettle.jad
打开生成的文件:
看静态块里的这句,INSTANCE = new Kettle("INSTANCE", 0);
,INSTANCE
确实是在类加载时执行静态代码块来舒适化的,同样是JVM来确保线程安全。
枚举类能不能用反射来破坏呢?不能。
写程序测试一下,注意,这里找构造函数的时候,传入参数不能是null
了,如果是null
的话,会报java.lang.NoSuchMethodException: singleton.enuma.Kettle.<init>()
错误。可以先打印一下枚举类的构造函数:
for (Constructor<?> c : Kettle.class.getDeclaredConstructors()) { System.out.println(c);}
得到结果private Kettle(java.lang.String,int)
。
为什么明明写的是无参构造函数,这里却显示有两个参数呢?这是因为所有的枚举类都继承自java.lang.Enum<E>
,这个类的构造函数有两个参数:
package java.lang;public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }}
从上面反编译出来的代码里也可以看到:
// jad verprivate Kettle(String s, int i){ super(s, i); empty = true; boiled = false;}
用这个类型的构造参数来获取:
Constructor<?> constructor = Kettle.class.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true);Object a = constructor.newInstance();Object b = constructor.newInstance();System.out.println(a == b);
程序报错为java.lang.IllegalArgumentException: Cannot reflectively create enum objects
。看源码Constructor.newInstance()
:
package java.lang.reflect;public final class Constructor<T> extends Executable { @CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { //... if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); //... }}
判断是否是枚举类,是枚举类的话,不允许通过反射来创建枚举对象。也就是同Java语言特性确保了枚举类实现的线程安全一样,确保了它不会被反射特性破坏。
用同样的测试程序来序列化和反序列化一下枚举类实现:
Kettle a = Kettle.INSTANCE;FileOutputStream fos = new FileOutputStream("Kettle.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(a);oos.flush();oos.close();FileInputStream fis = new FileInputStream("Kettle.obj");ObjectInputStream ois = new ObjectInputStream(fis);Kettle b = (Kettle) ois.readObject();ois.close();System.out.println(a == b);
打印出来的结果是true
!
查看源码,调用链如下:
package java.io;public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants { public final Object readObject() throws IOException, ClassNotFoundException { return readObject(Object.class); } private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException { ... Object obj = readObject0(type, false); ... return obj; ... }}
readObject0()
方法里,如果是enum类型:
/* java.io.ObjectInputStream.java */private Object readObject0(Class<?> type, boolean unshared) throws IOException { case TC_ENUM: ... return checkResolve(readEnum(unshared));}
readEnum()
,通过名字直接从enum类里获取枚举变量:
/* java.io.ObjectInputStream.java */private Enum<?> readEnum(boolean unshared) throws IOException { Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } } return result;}
所以反序列化回来的对象与原先枚举对象完全一致。