今天面试的时候被问到了单例模式,之前一直以为 static 关键字就是单例模式的实现。但实际上并不是。
什么是单例模式
单例模式是 Java 中的设计模式之一。这个设计模式涉及到一个单一的类,这个类负责创建自己的对象(构造方式是 private),同时确保只有单个对象被创建。这个类提供一个访问其唯一对象的方式,可以直接访问,不需要实例化这个对象。
- 单例类只有一个实例对象
- 单例类必须自己创建这个实例对象
- 单例类为其他对象提供这个唯一的实例
单例模式实现方法
懒汉式,线程不安全
1 | puclic class Singleton{ |
这段代码,实现了懒加载,但是有一个致命的缺点。在多线程并行调用 getInstance 方法时,就会创建多个实例。
懒汉式,线程安全
为了解决上面代码线程不安全的问题,我们可以将 getInstance 方法设置为同步的(使用 Synchronized)。
1 | puclic class Singleton{ |
这样虽然做到了线程安全,但是这样的方法效率很低。因为在任何时候只有一个线程能调用 getInstance 方法。但是同步的操作只需要在第一次调用的时候才需要,也就是在第一次实例对象被创建的时候是需要同步的,之后都可以直接判断是否已经创建了实例对象。这就引出了双重检测锁。
双重检测锁
这种方式有两次检查 instance == null
,一次在同步块外,一次在同步块内。因为可能多个线程一起进入同步块外的 if ,如果同步块内不做检查,就还是会创建多个实例对象。
1 | private static Singleton getInstance(){ |
事实上这段代码存在这一个问题 主要在于 instance = new Singleton()
这句,并并非是一个原子操作,事实上在 JVM 中这句话大概做了下面几件事:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量(创建实例对象)
- 将 instance 对象指向分配的内存空间 (执行完这步 instance 就是 非 null 的了)
在 JVM 的即时编译器中存在指令重排序的优化。也就是说 instance = new Singleton()
这句话不一定是按照 1-2-3这样顺序执行的,可能是 1-2-3 或者 1-3-2。如果是后者,则在 3 执行完毕、2 未指执行之前,当前线程被抢占了,这时 instance 已经是 非 null 的了(但是没有初始化),所以再调用 getInstance
得到的实际上是一个空对象,使用的时候自然就会报错。
解决这个问题只需要将 instance 对象声明成 volatile 就行了。
1 | private volatile static Singleton instance; //声明成 volatile |
volatile 可以禁止指令优化重排序。也就是在 volatile 变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。比如上面的列子,取操作必须在执行完 1-2-3 或者 1-3-2 之后,存在上面 1-3 之后取到值的情况。
饿汉模式 static final
这种方式非常简单,因为单例的实例被声明成了 static 和 final 的了,在第一次加载到内存中的时候就会被初始化,所以创建实例对象的本身是线程安全的。
1 | public class Singleton{ |
这种写法的缺点就是不是懒加载模式,单例会在加载类后一开始就初始化,即使客户端没有调用 getInstance
方法。这样的方式在一些场景中是无法使用的:
- Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance 之前必须调用每个方法设置参数给它。
静态内部类
1 | public class Singleton(){ |
这种写法依然是线程安全的,由于 SingletonHolder 是私有的,除了getInstance 之外的方法么有颁发访问他,因此这中方式是懒加载的。多线程同时读取实例是不会进行同步,没有性能缺陷,不依赖 JDK 版本。
枚举 Enum
1 | public enum EasySingleton{ |