单例模式有多种写法,线程安全以及性能好的写法主要有双重校验锁写法及静态内部类写法。
一、双重校验锁
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton (){
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
下面针对其中volatile、synchronized等关键字进行分析。
1、synchronized以及两次判空
if (singleton == null)出现了两次,第一次判断是为了避免每次都进行加锁,这样仅在第一次singleton为null时才加锁,第二次是为了在null的情况下创建实例,因为可能有多个线程均完成第一次判空,不加第二层判断也会创建多个实例。
2、volatile关键字作用
singleton = new Singleton()操作直接执行在主内存并不具备原子性,这行代码最终会被编译成多条汇编指令,大致做了3件事:
1)给Singleton的实例分配内存;
2)调用Singleton()的构造函数,初始化成员字段;
3)将singleton对象指向分配的内存空间(此时singleton就不是null了)。
如果不加volatile关键字,上面的第2和第3的顺序是无法保证的,当执行顺序为1-3-2时,假设A线程在3执行完毕、2未执行之前,被切换到B线程,这时singleton已经不是null了,却未初始化,B直接取走singleton,使用时就会出错,如下图,
先看变量如何在内存中存取数据,线程对变量的读写操作都在工作内存,不会直接操作主内存,也不会立即将工作内存的数据同步回主内存,下图为线程、工作内存、主内存三者的交互关系,
当singleton被声明为volatile,赋值部分字节码如下,
被volatile修饰后,赋值后(前面mov%eax,0x150(%esi)这句便是赋值操作)多执行了一个“lock addl0x0,(%esp)指令把修改同步到主内存时,所有之前的操作如分配内存和初始化都已经执行完成,即第3步完成前,第1和2步已经执行完,这样便形成了“指令重排序无法越过内存屏障”的效果。
所以被volatile修饰后,要么获取到的singleton为null,会阻塞在同步块,要么获取到初始化完成的对象。
二、静态内部类
1、静态类型饿汉式
先介绍静态类型饿汉式,因为静态内部类写法可看作静态类型饿汉式的懒加载形式,所以饿汉式的缺点是过早地创建了实例,从而降低了内存的使用率,代码如下,
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
实例初始化要从类加载说起,一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 、验证、准备、解析、初始化、使用和卸载七个阶段,如下
在变量初始化前,先要经过类加载,有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之 前开始):
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候。
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
- 调用一个类型的静态方法的时候。
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK8新加入的默认方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
根据第1条,要加载Singleton类型,基本上是在调用Singleton.getInstance()导致,但如果该类里面还有其他静态方法,或者其他静态字段,调用或者设置读取时也会导致类加载,类加载会导致instance实例被提前初始化出来,该过程为类变量初始化过程,在上面7个阶段的初始化阶段进行。我们接下来看一看各种类型变量(包括final以及static修饰)赋值是在什么时候。
1)最早一批是在准备阶段,JVM为被final和static同时修饰的基本类型变量赋值。如
public static final String value = “123”;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为“123”。
该阶段也会将仅被static修饰的变量赋初始值,如上value,去掉final修饰,
public static String value = “123”;
在准备阶段被赋初始值null。
2)接下来是仅被static修饰的变量,或者被final和static同时修饰的引用类型(引用对象),在初始化阶段被赋真实值。
在Javac编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句时,合并产生的<clinit>()方法,该方法为类构造器方法。
如下两种类型将在clinit方法中初始化
public static String value = “123”;
private static Singleton instance1 = new Singleton();
private static final Singleton instance2 = new Singleton();
value如1)在准备阶段初始化为null,在初始化阶段由clinit初始化为“123”,instance1和instance2均初始化为Singleton实例。
3)除以上两类,其他实例变量在实例创建过程中的内存分配后被初始化为零值,注意方法局部变量不会被初始化零值,如果不主动初始化,使用时会很危险。类实例接下来在初始化过程被构造函数<init>()初始化为代码指定值。
从上面第2条可以看出,由于instance为static类型,如果Singleton类提前加载,会使instance实例在类加载的初始化阶段就创建Singleton实例,而没有等到调用getInstance方法再实例化,这是该写法的主要缺点。
除了这一缺点,再考虑该写法未加锁会不会导致创建多个实例。
Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒后则不会再次进入<clinit>()方法。因此,同一个类加载器下,一个类型只会被初始化一次,即只会创建一个实例。
2、静态内部类
静态类型饿汉式只有一个缺点是没有达到懒加载的效果,静态内部类可以解决这个问题,如代码
public class Singleton {
private Singleton (){
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
}
这样,即使Singleton类新增了其他静态方法或者静态变量被使用而导致类被加载,也不会实例化自己。
三、枚举
上面介绍了3中单例模式,双重校验锁方式使用了volatile和synchronized有一定性能消耗,写法繁琐。静态类型饿汉式可能导致多余内存占用。静态内部类方式增加了新的类文件。
枚举单例不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,工作中使用的人很少,但极力推荐,如下
public class Singleton {
private Singleton() {}
//对外暴露一个获取User对象的静态方法
public static Singleton getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
//定义一个静态枚举类
private enum SingletonEnum {
//创建一个枚举对象,该对象天生为单例
INSTANCE;
private Singleton singleton;
// JVM保证这个方法绝对只调用一次
private SingletonEnum() {
singleton = new Singleton();
}
public Singleton getInstance() {
return singleton;
}
}
}