在日常开发中, 往往会出现这样一种情况, 就是在有些场景中, 为了节省频繁地创建对象而产生的系统开销, 会约定一个特定的类, 只能创建出一个实例而不应该创建多个实例. 这就应该使用单例模式了.
在 Java 开发中, 单例模式可以对上述场景进行了一个强制的保证. 可以通过巧用 Java 的现有语法, 能够保证一个类只能创建出一个实例这样的效果 (当程序员创建出多个实例的时候, 编译器就会报错).
在 Java 里实现单例模式的方式有很多种, 这里主要介绍最常见的两种.
饿汉模式
class Singleton {
// 先把实例创建出来
private static Singleton instance = new Singleton();
// 如果需要使用这个唯一实例, 统一通过 Singleton.getInstance() 获取
public static Singleton getInstance() {
return instance;
}
private Singleton() {}
}
在上述代码中, 首先在单例类中直接创建出类属性, 使用 static 修饰可以保证这个对象在 JVM 加载阶段就能构建出来. 使用了 private 修饰构造方法, 使构造方法私有化, 这样子就能保证在类外就不能手动创建出 Singleton 实例了, 如下图所示:
而是应该通过这个类的类方法 getInstance() 来获取到单例实例.
我们可以用以下代码来判断这个对象是不是单例的:
public class ThreadDemo19 {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
可以看到根据 getInstance() 方法获取到的对象是单例的
懒汉模式
根据上面饿汉模式的实现, 可以看出, 饿汉模式是在类加载阶段就将实例创建好了, 而懒汉模式将要实现的操作就应该是只有当这个类被使用的时候才去创建实例
class Singleton {
public static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() {
}
}
根据上述代码可以看出, 懒汉模式并没有在类加载阶段就将实例创建好, 而是在调用 getInstance() 方法的时候判断这个实例创建出来了没有, 如果没创建出来, 才会去创建这个实例, 也就是说, 只有第一次调用这个 getInstance() 方法的时候, 才会创建实例.
但是在上述代码的懒汉模式中, 有一个致命的问题:
- 那就是在多线程环境下, 同时调用 getInstance() 方法, 会不会产生多个实例?
线程安全问题
这里可以推断一下, 在多线程环境下, 饿汉模式和懒汉模式是否都存在线程安全问题?
多线程操作单例类, 只会调用 getInstance() 方法, 只要判断 getInstance() 方法是否是线程安全的, 那么使用的时候就是安全的.
先看饿汉模式:
public static Singleton getInstance() {
return instance;
}
可以看出, 在多线程环境下, 这个方法只有读操作, 不会引发线程安全问题.
但是反观懒汉模式:
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
其中 instance = new Singleton(); 这个操作是写操作, 在多线程环境下会出现线程安全问题
在 线程的状态和线程安全问题 一文中有详细的描写了什么情况下会产生线程安全问题.
上述代码中, 多个线程同时修改同一个变量, 就会导致线程安全问题.
if (instance == null) {
instance = new Singleton();
}
将上述操作以 cpu 的视角来看, 主要是四步操作:
- load, 将 instance 的值读取到寄存器中
- cmp, 将 instance 的值与 null 进行比较
- 如果 instance 的值为 null, 就进行 new 操作
- save, 将 instance 的值写回到内存中
如果多线程操作这个方法, 就可能会产生下面的结果:
当两个线程都在 instance 对象没创建的时候去调用 getInstance() 方法, 如果线程 t1 调用方法的时候, t2 在 t1 未 save 的情况下也调用了这个方法, 那么两个线程读取到寄存器中的 instance 值都为空, 那么两个线程都会进行 new 操作, 这样这个单例类就会创建出来两个实例了, 违背了单例模式的初衷.
加锁操作
那么要解决线程安全问题, 最主要的方法就是加锁!
将锁加在 if 条件的外面, 这样就能保证上述的四步操作都是原子的了.
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优化锁操作
但是这样写, 还会有一个问题:
- 就是每次调用 getInstance() 方法的时候, 都会进行一次加锁, 但是加锁所进行的开销是比较大的, 所以需要优化一下这个代码, 不用每次调用这个方法的时候都加上锁
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
内存可见性 和 指令重排序 问题
解决方法就是在锁的外面再加上一层判断, 判断 instance 对象是否已经被创建出来了, 如果已经被创建出来了, 就不用加锁了, 直接返回 instance 对象就行, 如果没创建出来, 就加锁并将这个对象创建出来再返回.
但是上述代码中, 还存在着一个问题:
- 内存可见性问题
- 指令重排序问题
内存可见性问题:
假设有很多个线程都去调用 getInstance() 方法, 这个时候会不会有被编译器优化的风险呢? (假设只有第一次读才是真正的去读了内存, 后续读都是去读寄存器, 这样也会创建出多个实例)
指令重排序问题:
将 instance = new Singleton(); 这个操作, 在编译器的视角可以拆成三步操作
- 申请内存空间
- 调用构造方法, 把这个内存空间初始化成一个合理的对象
- 把该内存空间的地址赋值给 instance 引用
正常情况下, 这行代码的三步操作应该是按照 1. 2. 3 顺序运行的, 但是编译器还会有一个操作, 就是指令重排序: 就是为了提高代码的执行效率, 会调整代码指令执行的顺序. 这样这个顺序就不一定按照 1. 2. 3 这样的顺序执行了, 可能按照 1. 3. 2 的顺序执行.
在单线程环境下, 指令重排序并不会影响代码的执行逻辑, 这个指令是 1. 2. 3 的顺序还是 1. 3. 2 的顺序执行并没有多大的关系. 但是在多线程环境下, 编译器容易进行误判, 这样就可能会触发指令重排序问题.
多线程中指令重排序问题可能会造成很大的影响, 因为线程是随机调度的, 这个指令可能并不会一下执行完这三条, 而是可能会执行完两条了, 其它线程就被调度进来执行其它的操作了.
假设 t1 执行完了 1. 3 两条指令, 申请了内存空间并把这个空间的地址赋值给变量了之后被调度走了, 这时 t2 也调用了 getInstance() 方法的时候, 这时进入判断, instance 并不是空的, 已经被赋值了一个内存地址, 那么就会直接返回 instance 对象. 但是, t1 并没有执行第二步操作: 调用构造方法, 把这个内存空间初始化成一个合理的对象, 这就导致了 t2 返回的是 t1 只执行了一半的结果, 这个 instance 对象是一个还没来得及初始化的 非法对象, 这时使用就会导致严重的后果.
为了解决 内存可见性 和 指令重排序问题, 可以给 instance 对象加上 volatile 关键字, 来告诉编译器操作这个对象的时候不能随意进行优化.
最后, 懒汉模式的完整代码就已经写完了
class Singleton {
public volatile static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
}