单例模式介绍
单例模式: 一种对象创建模式,为了确保系统中一个类只创建一个实例。
这样的好处是什么呢?
- 对于频繁使用的对象,可以进行复用;减少频繁创建对象和销毁所花费的时间和内存,减少了GC的压力。
一个成熟的单例模式要满足以下几点
- 单例类在系统中只能有一个实例
- 单例类必须自己创建自己的实例
- 单例类要提供这一实例给其他对象
注:注意单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。
单例模式的实现
1. 懒汉模式(懒加载-线程不安全)
public class SingletonDemo {
private static SingletonDemo instance;
//私有构造方法
private SingletonDemo(){}
public static SingletonDemo getInstance(){
if(instance == null){
instance = new SingletonDemo();
}
return instance;
}
}
上面代码使用private修饰了静态变量instance,让变量只可以在单例类内部调用,无参构造方法也使用private修饰,做到必须自己创建自己的实例;通过静态方法getInstance(),只创建一个实例类并且return出去;之所以称这种写法为懒汉模式,就是因为可以进行延迟加载,第一次调用才初始化,避免内存浪费。
但是这种写法是线程不安全的,在并发的情况下调用getInstance()方法,就有可能造成创建多个实例的情况。
针对这种情况,下面这种写法进行了优化。
2. 懒汉模式(懒加载-线程安全)
public class SingletonDemo {
private static SingletonDemo instance;
//私有构造方法
private SingletonDemo(){}
public static synchronized SingletonDemo getInstance(){
if(instance == null){
instance = new SingletonDemo();
}
return instance;
}
}
添加了synchronized关键字,使整个方法内的代码全部同步进行。
虽然添加synchronized解决了线程安全的问题,实现了真正的单例模式,但是方法执行一次后,再次调用就不再需要同步执行,效率很低,所以我们改进一下。
public class SingletonDemo {
private static SingletonDemo instance;
//私有构造方法
private SingletonDemo() {}
public static SingletonDemo getInstance() {
if (instance == null) {
//同步代码块
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
}
上面代码使用同步代码块synchronized (obj) 对一部分代码进行了加锁。
这就是双重检查锁(DCL),至于为什么要判断两次instance,大家设想一下两个线程的执行流程就明白了,这里就不多说了。
虽然上面使用同步代码块解决了并发时的效率问题,但是这里存在一个隐患,jvm为了提高执行效率,会进行指令重排序,指令重排序后,会打乱指令的先后执行顺序,我举个例子。
instance = new SingletonDemo();
上面创建对象的操作并不是一个原子操作,原子操作是指一个或者多个不可再分割的操作。这些操作的执行顺序不能被打乱,这些步骤也不可以被切割而只执行其中的一部分(不可中断性)。
而上面的指令执行顺序如下:
- A-分配内存空间
- B-初始化实例对象
- C-设置实例指向刚分配的内存地址(此时instance不等于null)
由于B和C都依赖于A,所以只会存在2种排序情况:
- A -> B -> C
- A -> C -> B
按照上面第一种分解顺序(A -> B -> C)不会有任何问题,是符合我们预期的,但是如果jvm按照第二种情况(A -> C -> B)执行,在多线程的情况下,就可能导致instance已经和初始对象内存建立关联,但是instance还没有进行初始化完成的情况,所以在判断if(instance == null)的时候,instance实际是不等于null的,这个时候如果使用instance实例,就会报错。
所以为了避免这种情况发生,我们需要用到 volatile 关键字。
使用了volatile的变量,会禁止jvm对其进行指令重排序,并且保证可见性。
可见性:java编程语言允许线程访问共享变量,为了确保共享变量的准确性和一致,其他线程能够立即看得到修改的值。
我们把代码修改一下,加上volatile,这样就解决了所有隐患,但是这种方式的效率不是很高。
public class SingletonDemo {
private static volatile SingletonDemo instance;
//私有构造方法
private SingletonDemo() {}
public static SingletonDemo getInstance() {
if (instance == null) {
//同步代码块
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
}
3. 饿汉模式(线程安全)
饿汉模式就是在类被加载的时候,就会创建实例,是线程安全的;缺点就是会在还不需要该实例的时候就已经把实例创建出来了,说白了就是不支持延迟加载。
public class SingletonDemo {
private static final SingletonDemo instance = new SingletonDemo();
//私有构造方法
private SingletonDemo() {}
public static SingletonDemo getInstance() {
return instance;
}
}
4. 静态内部类(懒加载-线程安全)
public class SingletonDemo {
private static class SingletonHolder {
private static final SingletonDemo INSTANCE = new SingletonDemo();
}
//私有构造方法
private SingletonDemo() {}
public static SingletonDemo getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类不会在单例类加载的时候就创建实例,而是在调用getInstance()方法的时候,也就是用到静态内部类的时候才会创建,这个时候jvm会加载SingletonHolder类,并且完成实例化的操作,在加载过程中,jvm会保证线程安全。
5. 枚举(线程安全)
枚举,支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,而且线程安全,是实现单例模式的最佳方法。
public enum SingletonDemo {
INSTANCE;
}
而在这几种实现方式中,不管是上面的双重检查锁还是静态内部类都可以用反射和反序列化进行暴力破解。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
而枚举不会被反射和反序列化破解。
Java中有规定:
枚举的序列化和反序列化是有特殊定制的,并且枚举是无法通过反射实现的。