1. 什么是单例模式
单例模式是指在内存中只会创建也且仅创建一次的对象的设计模式, 在程序中多次使用同一个多想且作用相同时,为了防止频繁地创建和销毁对象使得内存飙升,单例模式可以使程序仅在内存中创建一个对象,让所有调用该对象的地方都共享这一单例对象。
优点:
- 在内存中只有一个实例,减少了内存的开销,尤其是频繁地创建和销毁实例,同时也减少了GC
- 避免对资源的多重占用,例如对文件的操作
- 通过这个全局访问点为抓手,可以对资源的访问做优化处理,例如访问点中预置缓存
缺点:
- 没有接口,不能继承,使扩展困难,违背了开闭原则
- 单例模式的功能一般写于一个类中,这会导致业务逻辑耦合,与单一责任原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么实例化
- 单例模式只存在一个实例,在并发测试中不利于调试
2. 单例模式的实现方式
1. 懒汉式
在真正需要使用对象时才去创建该单例对象,懒汉式创建对象的方法是在程序使用对象前,先判断对象是否已经实例化(判空),若已实例化则直接返回该类对象,否则执行实例化操作。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式是单例模式的最简单的实现方式,但这种实现方式在并发情况下并不安全,如果两个线程同时判断对象实例为null, 那么两个线程就会分别实例化对象,从严格意义上来说它并不属于单例模式。
所以我们要解决的是线程安全问题,最简单的方式加锁–synchronized。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这样就规避了两个线程同时去实例化对象的风险,但是这会引来另一个问题:每次获取对象都需要先获取锁,并发性能非常差,效率非常低,因为99%情况下是不需要同步的
2. 饿汉式
饿汉式在类加载时就已经创建好该对象,在调用获取对象实例时直接返回该单例对象即可,不需要等到被调用时才去创建。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
这种方式基于classloader机制避免了多线程的同步问题,不会产生多个Singleton对象,也不需要加锁,执行效率会提高,但是在类加载时就初始化,会浪费内存。
3. 双重校验锁(DCL, double-checked locking)
双重校验锁机制其实是懒汉式的一种升级,不仅保证了线程安全,并且在多线程情况下能保持高性能。
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singlenton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面的代码可以完美解决并发安全和性能低效的问题,
- 如果instance不为空,则直接返回对象,不需要获取锁,如果多个线程发现instance为空,则进入if分支
- if分支内,多个线程会去抢同一个Class锁, 只有一个线程会抢成功,获取到class锁的线程会再次判断instance是否为空,因为instance有可能被之前线程实例化了
- 之后获取到class锁的线程,会发现instance已经不为空了,则不会再去实例化对象,直接返回
- 之后所有的进入该方法的线程都不会获取锁,因为instance已经被实例化了,不会为null
volatile 防止指令重排序
首先了解下指令重排序是什么。
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序性能
在JVM创建对象时,会经历以下步骤:
- 判断对象对应的类是否加载、链接、初始化
- 为对象分配内存
- 处理并发问题
- 初始化分配到的内存
- 设置对象的对象头
- 执行init方法进行初始化
在第6步,需要初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。这有可能会发生指令重排序现象,比如先把堆内对象的首地址赋值给引用变量,然后再初始化对象,有可能在线程A先把堆内对象的首地址赋值给引用变量,此时线程B判断instance并不为空,但实际上线程A并还没完全初始化Singleton对象,线程B就会报NPE异常。
4. 静态内部类
这种方式可以实现跟双重校验锁方式一样的效果,和饿汉式同样利用了classloader机制来保证线程安全,但是又有点不一样。饿汉式只要Singleton类被装载,那么instance就会被实例化,而这种方式,即使Singleton类被装载了,但是instance不一定被实例化,因为SingletonHolder类没有被主动使用,只有显式地调用getInstance方法时才会显示地装载SingletonHolder,从而实例化instance。如果instance很耗资源想让他延迟加载,另一方面又不想Singleton类加载时就实例化,因为Singleton可能在其他地方会显式调用,那么这时候这种方式比饿汉式是更加合理的。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
5. 枚举
public enum Singleton {
INSTANCE;
Singleton(){}
}
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,也不能通过 reflection attack 来调用私有构造方法。
枚举是如何实现单例的呢?
在程序启动时,会调用枚举类的空参构造器,实例好一个Singleton对象赋给INSTANCE,之后再也不会实例化
枚举是如何防止反射的呢?
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
另外枚举类的无参构造器并不是真的无参,编译后的枚举类构造器的参数是(String.class, int.class)
枚举类是如何防止反序列化的呢?
在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
破坏单例模式
反射破坏单例模式
饿汉式和懒汉式
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance3 = constructor.newInstance();
System.out.println(instance1 == instance2); // true
System.out.println(instance1 == instance3); // false
}
枚举
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance3 = constructor.newInstance();
System.out.println(instance1 == instance2);
System.out.println(instance1 == instance3);
}
Exception in thread “main” java.lang.NoSuchMethodException: com.example.demo.design.pattern.singleton.enums.Singleton.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.example.demo.design.pattern.singleton.enums.TestEnums.main(TestEnums.java:25)
错误显示没有无参构造器,经过反编译来看,枚举类构造器确实是有参的
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Singleton instance3 = constructor.newInstance();
System.out.println(instance1 == instance2);
System.out.println(instance1 == instance3);
}
报错不允许反射创建枚举类, 源码:Constructor.newInstance()
Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.example.demo.design.pattern.singleton.enums.TestEnums.main(TestEnums.java:27)
反序列化破坏单例模式
饿汉式和懒汉式
public static void main(String[] args){
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
outputStream.writeObject(Singleton.getInstance());
File file = new File("Singleton.file");
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
Singleton instance = (Singleton) inputStream.readObject();
System.out.println(instance == Singleton.getInstance()); // false
}
枚举
public static void main(String[] args) {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
outputStream.writeObject(Singleton.INSTANCE);
File file = new File("Singleton.file");
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
Singleton instance = (Singleton) inputStream.readObject();
System.out.println(instance == Singleton.INSTANCE); // true
}