单例模式
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。
1. 单例模式的结构
单例模式主要有以下角色:
| 角色 | 职责 |
|---|---|
| 单例类 | 只能创建一个实例的类 |
| 访问类 | 使用单例类 |
2. 单例模式的实现
单例设计模式分为两种:
- 饿汉式:类加载就会导致该单实例对象被创建;
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。
2.1 饿汉式-方式1(静态变量方式)
/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置创建该类的对象
private static Singleton instance = new Singleton();
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}说明:该方式在成员位置声明 Singleton 类型的静态变量,并创建 Singleton 类的对象 instance。instance 对象是随着类的加载而创建的。如果该对象足够大,而一直没有使用,就会造成内存的浪费。
2.2 饿汉式-方式2(静态代码块方式)
/**
* 饿汉式
* 在静态代码块中创建该类对象
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置声明该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}说明:该方式在成员位置声明 Singleton 类型的静态变量,而对象的创建是在静态代码块中,也是随着类的加载而创建。所以和饿汉式的方式 1 基本上一样,该方式同样存在内存浪费问题。
2.3 懒汉式-方式1(线程不安全)
/**
* 懒汉式
* 线程不安全
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置声明该类的对象
private static Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}说明:从上面代码可以看出,该方式在成员位置声明 Singleton 类型的静态变量,并没有进行对象的赋值操作。那么什么时候赋值呢?当调用 getInstance() 方法获取 Singleton 类的对象时才创建对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
2.4 懒汉式-方式2(线程安全)
/**
* 懒汉式
* 线程安全
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置声明该类的对象
private static Singleton instance;
// 对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}说明:该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在 getInstance() 方法上添加了 synchronized 关键字,导致该方法的执行效率特别低。从上面代码可以看出,其实只有在初始化 instance 的时候才会出现线程安全问题,一旦初始化完成就不存在了。
2.5 懒汉式-方式3(双重检查锁)
再来讨论一下懒汉模式中加锁的问题:对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式。
/**
* 双重检查方式
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
private static Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
// 第一次判断,如果 instance 不为 null,不进入抢锁阶段,直接返回实例
if (instance == null) {
synchronized (Singleton.class) {
// 抢到锁之后再次判断是否为 null
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。但上面的双重检查锁模式看上去完美无缺,其实是存在问题的:在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来的空指针异常问题,只需要使用 volatile 关键字,它可以保证可见性和有序性。
/**
* 双重检查方式
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
private static volatile Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
// 第一次判断,如果 instance 不为 null,不进入抢锁阶段,直接返回实例
if (instance == null) {
synchronized (Singleton.class) {
// 抢到锁之后再次判断是否为 null
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}重要
添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,既能保证多线程下的线程安全,也不会有性能问题。
2.6 懒汉式-方式4(静态内部类方式)
静态内部类单例模式中实例由内部类创建。由于 JVM 在加载外部类的过程中,不会加载静态内部类,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
/**
* 静态内部类方式
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}说明:第一次加载 Singleton 类时不会去初始化 INSTANCE,只有第一次调用 getInstance() 时,虚拟机才会加载 SingletonHolder 并初始化 INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
提示
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
2.7 枚举方式
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分利用了枚举的这个特性来实现单例模式。枚举的写法非常简单,而且枚举类型是所有单例实现中唯一一种不会被破坏的单例实现模式。
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}说明:枚举方式属于饿汉式。
2.8 实现方式对比
| 实现方式 | 懒加载 | 线程安全 | 性能 | 备注 |
|---|---|---|---|---|
| 饿汉式(静态变量/静态代码块) | 否 | 是(类加载机制保证) | 好 | 可能造成内存浪费 |
| 懒汉式(线程不安全) | 是 | 否 | 好 | 只适用于单线程 |
| 懒汉式(方法加 synchronized) | 是 | 是 | 差 | 每次获取实例都要抢锁 |
| 双重检查锁 + volatile | 是 | 是 | 好 | 推荐使用 |
| 静态内部类 | 是 | 是(类加载机制保证) | 好 | 推荐使用 |
| 枚举 | 否 | 是 | 好 | 唯一不可被破坏的写法 |
3. 存在的问题
3.1 问题演示
破坏单例模式:使上面定义的单例类(Singleton)可以创建多个对象(枚举方式除外)。有两种方式,分别是序列化和反射。
3.1.1 序列化反序列化破坏单例
Singleton 类:
import java.io.Serializable;
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}测试类:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Test {
public static void main(String[] args) throws Exception {
// 往文件中写对象
// writeObject2File();
// 从文件中读取对象
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
// 判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
private static Singleton readObjectFromFile() throws Exception {
// 创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
// 读取 Singleton 对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
public static void writeObject2File() throws Exception {
// 获取 Singleton 类的对象
Singleton instance = Singleton.getInstance();
// 创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
// 将 instance 对象写出到文件中
oos.writeObject(instance);
}
}运行结果是 false,表明序列化和反序列化已经破坏了单例设计模式。
3.1.2 反射破坏单例
Singleton 类:
public class Singleton {
// 私有构造方法
private Singleton() {}
private static volatile Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if (instance != null) {
return instance;
}
synchronized (Singleton.class) {
if (instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}测试类:
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
// 获取 Singleton 类的字节码对象
Class<Singleton> clazz = Singleton.class;
// 获取 Singleton 类的私有无参构造方法对象
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
// 取消访问检查
constructor.setAccessible(true);
// 创建 Singleton 类的对象 s1
Singleton s1 = constructor.newInstance();
// 创建 Singleton 类的对象 s2
Singleton s2 = constructor.newInstance();
// 判断通过反射创建的两个 Singleton 对象是否是同一个对象
System.out.println(s1 == s2);
}
}运行结果是 false,表明反射已经破坏了单例设计模式。
注意
枚举方式不会出现上面这两个问题。
3.2 问题的解决
3.2.1 序列化、反序列化破坏单例模式的解决方法
在 Singleton 类中添加 readResolve() 方法。它在反序列化时会被反射调用:如果定义了这个方法,就返回这个方法的值;如果没有定义,则返回新 new 出来的对象。
Singleton 类:
import java.io.Serializable;
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}源码解析(ObjectInputStream 类):
public final Object readObject() throws IOException, ClassNotFoundException {
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false); // 重点查看 readObject0 方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared)); // 重点查看 readOrdinaryObject 方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
// isInstantiable 返回 true,执行 desc.newInstance(),通过反射创建新的单例类
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在 Singleton 类中添加 readResolve 方法后,desc.hasReadResolveMethod() 方法执行结果为 true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给 rep 变量
// 这样多次调用 ObjectInputStream 类中的 readObject 方法,就会调用我们定义的 readResolve 方法,所以返回的是同一个对象
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}3.2.2 反射破坏单例的解决方法
public class Singleton {
// 私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if (instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if (instance != null) {
return instance;
}
synchronized (Singleton.class) {
if (instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}说明:这种方式比较好理解。当通过反射方式调用构造方法进行创建时,直接抛异常,不允许此类操作。
4. JDK 源码解析 —— Runtime 类
Runtime 类就是使用的单例设计模式。
4.1 Runtime 使用的是哪种单例模式
通过源代码查看:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}从上面源代码中可以看出,Runtime 类使用的是饿汉式(静态变量)方式来实现单例模式的。
4.2 使用 Runtime 类中的方法
import java.io.IOException;
import java.io.InputStream;
public class RuntimeDemo {
public static void main(String[] args) throws IOException {
// 获取 Runtime 类对象
Runtime runtime = Runtime.getRuntime();
// 返回 Java 虚拟机中的内存总量
System.out.println(runtime.totalMemory());
// 返回 Java 虚拟机试图使用的最大内存量
System.out.println(runtime.maxMemory());
// 创建一个新的进程执行指定的字符串命令,返回进程对象
Process process = runtime.exec("ipconfig");
// 获取命令执行后的结果,通过输入流获取
InputStream inputStream = process.getInputStream();
byte[] arr = new byte[1024 * 1024 * 100];
int b = inputStream.read(arr);
System.out.println(new String(arr, 0, b, "gbk"));
}
}5. 小结
- 单例模式的核心是"私有构造器 + 静态实例 + 静态获取方法",按创建时机分为饿汉式与懒汉式两大类;
- 懒汉式要重点掌握双重检查锁 + volatile 与静态内部类两种写法:前者理解
volatile防止指令重排序的意义,后者利用类加载机制天然保证线程安全; - 枚举写法最简洁,且是唯一无法被序列化/反射破坏的单例实现,生产环境中可以优先考虑;
- JDK 中的
Runtime类就是饿汉式单例的典型应用,阅读其源码可以加深对单例模式的理解。
