前言
最近在看《深入理解Java虚拟机》一书,看到了类初始化的五种情况。联想到之前看到过通过静态内部类来实现单例懒加载的模式,当时不理解其原理,现在特此记录一下。
由传统单例引起
传统的单例模式分为饿汉,懒汉模式。因为synchronized 关键字,导致性能被浪费。在实例化之后的对象读取,希望是无锁的。所以引入“双检锁模式”
public class Singleton{
private static Singleton singleton;
private Singleton() {
}
public synchronized static Single newInstance() {
if (singleton== null) {
singleton= new Singleton();
}
return singleton;
}
}
双检锁
但是,这种双检锁模式也会存在问题,多线程情况下,会出现某个线程获取到的实例对象虽然被赋予默认值,但其初始化还未完成的情况。这种问题的解决办法则是添加volatile关键字,但volatile也会有性能损耗。
public class DoubleCheckedLock {
private static DoubleCheckedLock instance;
public static DoubleCheckedLock getInstance(){
if (null == instance){
synchronized (DoubleCheckedLock.class){
if (null == instance){
instance = new DoubleCheckedLock();
}
}
}
return instance;
}
}
静态内部类
这种方式通过jvm来保证其线程安全,而懒加载的机制则是依靠虚拟机规范制度的类“初始化”规则保证
public class Singleton {
private static class SingletonHolder {
private static Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton newInstance() {
return SingletonHolder.singleton;
}
}
类初始化规则
虚拟机规范则严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要
在此之前开始):
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初
始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字
实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常
量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 - 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,
则需要先触发其初始化。 - 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父
类的初始化。 - 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个
类),虚拟机会先初始化这个主类。 - 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后
的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄
所对应的类没有进行过初始化,则需要先触发其初始化。
参考:《深入理解Java虚拟机》