单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这个最简单的设计模式,确是很容易让人忽略的一个点。其实其中很多种实现方式涉及到的原理还是比较深的。这次就来看看各种单例模式的实现之间的区别。
1. 懒汉式
public class Singleton {
private static Singleton instance;
public static Singleton getInstance(){
if(instance== null){
instance= new Singleton();
}
return instance;
}
}
最简单的懒汉式,可以起到延时加载的功能。在高并发场景下,可能会有多个线程同时进入if(instance== null),那么就会生成多个单例实例,不符合单例模式的思想,故不推荐。
2. 饿汉式加锁
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
instance = new Singleton();
}
}
}
return instance;
}
}
能看到,我这里给创建单例实例的代码加了锁。保证了同时只能有一个线程去创建这个实例。表面上看是没问题的,但其实依旧有线程安全的问题。 问题就出在JVM的指令重排上。
对于 instance= new Singleton(); 这个语句,我们可以把它简单的分为三个步骤。
1. 在堆内存中开辟一块空间
2. 创建实例,并把实例数据放入刚开辟的堆内存中
3. 将 instance变量指向堆内存的这块空间
但是在经过指令重排后,创建对象实际的执行顺序可能会变为 1 --> 3 --> 2。在单线程的场景下,这样并不会有任何问题。但在多线程场景下,我们思考一个问题。
假如线程1进入同步代码块,并以 1 --> 3 --> 2 的顺序去创建这个对象。那么在给instance变量赋值时,这个对象在目标区域并没有生成,这时instance变量的指针指向了一个空闲的内存区域。
那么这时,线程2去判断 instance== null 时,是无法通过的,因为这时instance已经变赋值过了。那么,线程2就会得到一个空的对象,操作此对象时,就会出现异常了。
3. 双重校验锁
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return instance;
}
}
与上面的区别在于给instance变量加了volatile修饰。volatile禁止了指令重排,既创建对象一定是以1-2-3的顺序执行,这样就不会出现线程安全问题。 双重校验锁也是比较推荐的一种写法。
4. 饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
饿汉模式的优点在于天然的拥有线程安全问题,缺点在于无法实现延时加载。网上有很多地方对无法实现延时加载这个点没有说清楚。实际上在理想情况下,饿汉模式也是一种延时加载,为何呢?
java的类加载分为三个阶段,分别是加载,连接和初始化。而类的静态变量的初始化和静态代码块的执行是在初始化这个步骤进行的。而触发类初始化的条件在虚拟机规范中有明确说明。
- new了一个类的实例
- 调用了类的非final的静态变量或静态方法
- 通过反射对该类进行了调用
- 初始化一个类的子类时,父类会在子类初始化之前进行初始化。
- 当该类是启动类(包含main方法的类)时。
- 使用jdk1.7的动态语言支持时
也就是说,在没有对类进行以上六种操作是,是不会触发类的初始化行为的。那么我们的单例对象依然为空。当我们第一次调用 getIntance方法时,JVM就会提前创建instance对象的实例。这实际上就是一种延时加载了。但是为什么又说他无法保证延时加载呢。因为我们无法保证这个类是否拥有其他非final的静态变量和方法,我们也无法阻止程序通过反射的方式对类进行操作,或者是new了这个类子类的实例。所以当我们对该类进行了以上的操作,就算我们的本意不想实例化单例对象,JVM也会把实例给创建了。这样也就违背了延时加载的原则。
但是,在一些对延时加载要求并不苛刻的场景下,使用饿汉模式也是一种好的选择。
5. 静态内部类
public class Singleton {
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
静态内部类的方式保证了只有在调用 getInstance方法时,才将创建单例实例,确保了延时加载。是推荐使用的一种方式。
6. 枚举类
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
这种方式利用的枚举类的特性,天生的拥有了单例和线程安全两种特性,同时还能避免被反射调用,是推荐使用的一种方式。