类的加载时机汇总

1,628 阅读3分钟

实践Tip:通过单例模式引入JVM类加载机制的内容。

静态内部类单例模式是如何实现线程安全的?

首先,需要了解类的加载时机。

类的加载时机

Java 虚拟机在有且仅有的 5 中场景下会对类进行初始化。

1、遇到 new、getstatic、setstatic 或 invokestatic 这 4 个字节码指令时,分别对应如下Java代码场景:

new : new一个实例化对象

getstatic 读取一个静态字段(final修饰、已在编译期把结果放入常量池的除外)

setstatic 设置一个静态字段(同上)

invokestatic 调用一个类的静态方法

2、使用 java.lang.reflect 包中的方法,对类进行反射调用时,如果类没有初始化过,会触发初始化之。

3、当初始化一个类时,如果父类未初始化,会先触发父类的初始化。

4、当虚拟机启动时,用户需要制定一个要执行的主类(含 main 方法的类),虚拟机会先初始化这个类。

5、当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle 
实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,
并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上5中情况被称为类的主动引用。除此之外的所有对类的引用都不会对类进行初始化,被成为被动引用。

静态内部类就是被动引用的类型。

当 getInstance() 方法被调用时,SingletonHolder 才在 Singleton 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 INSTANCE 也才最终被创建,然后再被 getInstance() 方法返回。

Q:INSTANCE 在创建过程中又是如何保证线程安全的呢?

《深入理解JVM》引用:

JVM 保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步。

如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>() 方法,
其他线程都需要阻塞等待,直到活动线程执行完 <clinit>() 方法。

当某个线程第一次执行了<clinit>() 这个初始化方法之后,其他线程就不会重复执行初始化了。

同一个加载器下,一个类只会被初始化一次。

因此,一个静态内部类被加载初始化时,从类加载器的角度来看,是线程安全的,而单例对象 INSTANCE 在这个静态内部类中是一个静态 Field,所以它跟随这个静态内部类的加载而初始化,且只在类加载时初始化一次。

静态内部类初始化的缺陷

传参问题:

由于是静态内部类的形式去创建单例的,所以外部无法传递参数进去,如 Context 这种参数。

因此,在创建单例时,可以在静态内部类与 DCL 模式之间权衡选择。