单例模式
单例模式
单例模式, 顾名思义就是只有一个实例, 并且它自己负责创建自己的对象, 这个类提供了一种访问其唯一的对象的形式. 可以直接访问, 不需要实例化该类的对象.
单例模式的几种形式
饿汉式
class Singleton { private Singleton() { } private static Singleton instance = new Singleton(); public static Singleton newInstance() { return instance; } }
实例在类初始化的时候就创建好了, 不管你有没有用到. 好处是没有线程安全问题, 坏处是比较浪费内存空间.
懒汉式
class Singleton {
private Singleton() { }
private static Singleton instance;
public static Singleton newInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式, 顾名思义就是实例在用到的时候才去创建.
有线程安全和不安全两种写法, 区别就是synchronized
关键字
双检锁
class Singleton {
private Singleton() { }
private static Singleton instance;
public static Singleton newInstance() {
if (instance == null) { // 第一次检查, 有则返回 (此时尚未加锁, 可以提高效率)
synchronized(Singleton.class) {
if (instance == null) { // 第二次检查, 再获取到锁之间, 再次检查是为完成初始化
instance = new Singleton();
}
}
}
return instance;
}
}
双检锁, 又叫双重校验锁, 综合了饿汉式和懒汉式两者的优缺点整合而成, 从上面的代码实现看, 特点是在 synchronized
关键字内外都加了一层 if条件判断, 这样既保证了线程安全, 又比直接上锁提高了执行效率, 还节省了内存空间. (其实有坑)
静态内部类
class Singleton {
private Singleton() { }
private static class Inner {
private static Singleton instance = new Singleton();
}
public static Singleton newInstance() {
return instance;
}
}
- 静态内部类不会随着外部类的初始化而初始化, 它是需要单独去加载和初始化的, 当第一次执行
getInstance()
方法时,Inner
类会被初始化. - 静态对象
instance
的初始化在Inner
类初始化阶段进行, 类初始化阶段即虚拟机执行类构造器<client>()
方法的过程. - 虚拟机会保证一个类的
<client>()
方法在多线程环境下被正确地加锁和同步, 如果多个线程同时初始化一个类, 只会有一个线程执行这个类的<client>
方法, 其他线程都会阻塞等待.
枚举
enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("do something ...");
}
}
枚举的方式是比较少见一种的实现方式. 但是看上面的代码, 却简洁清晰. 并且它还自动支持序列化机制, 绝对防止多次实例化.
传统单例模式双重检查锁存在的问题
单例模式 1.0
class Singleton {
private Singleton() { }
private static Singleton instance;
public static Singleton newInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式很辣鸡, 因为多线程环境下不能保证单例.
单例模式 2.0
class Singleton {
private Singleton() { }
private static Singleton instance;
public static synchronized Singleton newInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式也很辣鸡, 因为多线程环境下, 每个线程执行 getInstance()
都要阻塞, 效率很低.
单例模式 3.0
class Singleton {
private Singleton() { }
private static Singleton instance;
public static Singleton newInstance() {
if (instance == null) { // 位置1
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 位置2
}
}
}
return instance;
}
}
这种方式使用双重检查锁, 多线程环境下执行 getInstance()
时, 先判断单例对象是否已初始化, 如果已经初始化, 就直接返回单例对象, 如果未初始化, 就在同步代码快中先完成初始化, 然后返回, 效率很高.
但是: 这种方式是一个错误的优化, 问题的根源出现在位置2,
instance = new Singleton();
这句话创建了一个对象, 它可以分解成如下3行代码:
memory = allocate(); // 1. 分配对象的内存空间
ctorInstance(memory); // 2. 初始化对象
instance = memory; // 3. 设置 instance指向刚分配的内存地址.
上述伪代码中的 2和3之间可能发生重排序, 重排序后的执行顺序如下.
memory = allocate(); // 1.分配对象的内存空间
instance = memory; // 2.设置instance指向刚分配的内存地址,此时对象还没有被初始化
ctorInstance(memory); // 3.初始化对象
因为这种重排序并不影响 java规范中的规范, intra-thread sematics
允许那些在单线程内不会改变单线程程序执行结果的重排序.
但是多线程并发时可能会出现以下情况: 线程B访问到的是一个还未初始化的对象
解决方案1:
将对象声明为 volatile后, 前面的重排序在多线程环境下将被禁止.
class Singleton {
private Singleton() { }
private static volatile Singleton instance;
public static Singleton newInstance() {
if (instance == null) { // 位置1
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 位置2
}
}
}
return instance;
}
}
解决方案2:
class Singleton {
private Singleton() { }
private static class Inner {
private static Singleton instance = new Singleton();
}
public static Singleton newInstance() {
return instance;
}
}
- 静态内部类不会随着外部类的初始化而初始化, 它是需要单独去加载和初始化的, 当第一次执行
getInstance()
方法时,Inner
类会被初始化. - 静态对象
instance
的初始化在Inner
类初始化阶段进行, 类初始化阶段即虚拟机执行类构造器<client>()
方法的过程. - 虚拟机会保证一个类的
<client>()
方法在多线程环境下被正确地加锁和同步, 如果多个线程同时初始化一个类, 只会有一个线程执行这个类的<client>
方法, 其他线程都会阻塞等待.
似乎静态内部类看起来已经是最完美的方法了, 其实不是, 可能还存在反射攻击或者反序列化攻击. 且看如下代码:
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton); // 运行结果: false
}
通过结果看, 这两个实例不是同一个, 这就违背了单例模式的原则了. 出了反射攻击之外, 还可能存在序列化攻击的情况.
class Singleton8 implements Serializable {
private Singleton8() {}
private static class SingletonHolder {
private static Singleton8 instance = new Singleton8();
}
public static Singleton8 getInstance() {
return SingletonHolder.instance;
}
public static void main(String[] args) {
Singleton8 instance = Singleton8.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton8 newInstance = SerializationUtils.deserialize(serialize);
System.out.println(instance == newInstance); // false
}
}
解决方案3:
通过枚举实现单例模式
在 <<Effective Java>>
书中说道, 最佳的单例实现模式就是枚举模式. 利用枚举的特性, 让 JVM来帮我们保证线程安全和单一实例. 初次之外, 写法还特别简单.
enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("do something ...");
}
}