引言
为了更好的理解Spring框架里的IOC、AOP,最好先学习一些常见的设计模式(因为Spring源码里面设计模式满天飞),第一个就是大名鼎鼎的单例模式啦。
单例模式概念
单例模式简单来说就是一个类只有一个实例,单例模式遵循的原则如下:
- 整个类只能有一个实例,因此无法通过new()任意创建对象,构造函数为private。整个类维护的唯一实例在类的内部,且用static修饰。(若非static,还需在类外部先创建对象,再访问)
- 这个实例提供一个公共访问接口,通过这个接口可以访问到唯一的这个实例。这个访问方法也是静态的,理由同上。
单例模式的应用场景
单例模式可以解决的问题有以下几个:
- 有一些类的对象会被频繁创建和销毁,频繁的加载、初始化、销毁开销比较大,如果只需维护一个实例,就节省了那些开销。
- 对于“资源管理者”,单个实例便于管理资源、避免了同步问题。比如网站计数器、线程池、垃圾回收站、文件系统、日志。
五种单例模式
(一)饿汉式
饿汉式是最简单粗暴的单例模式,直接用一个静态域来存储整个类的唯一实例:
public class HungrySingleton{
// 用静态域存储唯一实例
private static final HungrySingleton instance = new HungrySingleton();
// 私有构造方法
private HungrySingleton(){
}
// 提供一个对外访问接口
public final HungrySingleton getInstance(){
return instance;
}
}
之所以称之为饿汉式,是因为这个类还没等到人家访问他的实例,就急着在初始化的时候创建了实例。如果有很多单例类,每个类都这样的话,会非常浪费空间,因为他们的实例创建出来以后很可能并不急着用,白白占着内存。
因此,为了避免空间浪费,出现了懒汉式。instance在类加载、初始化的时候生成且生成后无法改变,所以不存在线程安全问题。代码中用final修饰了instance变量,如果需要释放instance资源的话则可以不加final。
(二)懒汉式
懒汉式相对饿汉式最大的区别就在于延迟加载,即一开始不急着创建实例,而是等到有人访问的时候再创建。最简单的懒汉式可以这么写:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){
}
public LazySingleton getInstance(){
if (instance == null)
instance = new LazySingleton();
return instance;
}
}
上面的写法在单线程环境下没有任何问题,但是很显然在多线程环境下,如果多个线程同时判断出instance == null
,会创建出多个实例,因此需要引入锁机制。
最简单粗暴的做法就是在 getInstance() 方法前面加一个Synchronized
修饰语,使其变为一个同步方法。但问题是,当instance不为null后,已经没有线程安全的隐患了,但多个线程想要同时访问instance仍然需要拿到锁,并发率太低,效率低下,因此需要把线程锁加到更小的范围内,比如下面这样:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){
}
public LazySingleton getInstance(){
if (instance == null){
synchronized (LazySingleton.class){
if (instance == null)
instance = new LazySingleton();
}
}
return instance;
}
}
要注意的是如果在synchronized
修饰的代码块里不再次判断instance == null
的话,刚才所提到的线程安全问题还是没有得到解决,因为在第一次冲突发生的时候,后面拿到锁的线程依旧会创建实例。
至此,懒汉式的线程安全问题得到解决,但是新的问题出现了:instance = new LazySingleton();
这个语句不是一个原子操作,分为好几个步骤。下面做一个小实验来证明:
声明类A,在main函数里new一个实例并复制给变量a,代码如下:
public class A {
public A(){}
public static void main(String[] args) {
A a = new A();
}
}
接下来javap -v A.class
, 反编译生成的class文件,得到如下的结果:
可以看到 A a = new A()
这个语句分为四个步骤:
-
new指令:在java堆上为 A 类的对象分配内存空间,并将地址压入操作数栈顶;
-
dup指令:复制操作数栈顶的值,并将其压入栈顶;
-
invokespecial指令:调用实例初始化方法,弹出一个对象地址;
-
astore_1 指令:从操作数栈顶取出 A对象的引用并存到局部变量表;
由于JVM会进行指令重排序(出于优化目的),所以这三个步骤的相对顺序不是固定的。因此需要考虑到的一种情况是:以上步骤发生的顺序是1->2->4->3,而在4与3之间另一个线程调用getInstance()
方法,此时instance不为null,于是直接返回未初始化完毕的instance,便会引发问题。
为了解决JVM指令重排序的问题,一个很方便的解决方案是使用volatile修饰词,volatile也属于锁,可以禁止指令重排序,因此就出现了第三种单例模式——双重锁检验单例模式。
(三)双重锁检验机制
很简单,在刚才的基础上加一个volatile关键字来修饰instance就行。
public class LazySingleton {
private static volatile LazySingleton instance = null;
private LazySingleton(){
}
public LazySingleton getInstance(){
if (instance == null){
synchronized (LazySingleton.class){
if (instance == null)
instance = new LazySingleton();
}
}
return instance;
}
}
(四)静态内部类懒汉式
懒汉式为了解决线程安全+指令重排序的问题,还有一个更简便的方法就是使用静态内部类,如下所示:
public class StaticClassSingleton {
private static class CreateInstance{
private static final StaticClassSingleton instance = new StaticClassSingleton();
}
private StaticClassSingleton(){
}
public static final StaticClassSingleton getInstance(){
return CreateInstance.instance;
}
}
静态内部类只有在被访问的时候才会进行初始化(详情可参考JVM虚拟机类初始化的时机),因此属于延迟加载。
这种方法之所以能在多线程环境下保证线程安全,是因为JVM保证类的初始化是线程安全的,因此instance只能被创建一次。
(五) 枚举式
public enum EnumSingleton{
INSTANCE,
}
枚举类里面所枚举的对象都是单例的,这里的INSTANCE在JVM中会变成public static final EnumSingleton INSTANCE;
。在上面的代码里只枚举了一个对象,而枚举类的构造函数是默认并强制私有的,无法在类外部创建对象,因此整个类只有INSTANCE一个对象。
访问时直接用EnumSingleton.INSTANCE即可。枚举类是实现单例模式最便捷的方式,同时还保证了安全(没有线程安全问题,阻止反序列化或反射创建新对象),但是没有延迟加载。