一、单例
单例是一个常见的设计模式,常见有四种方式来实现,即懒汉式、饿汉式、枚举和静态内部类实现,这个模式的本质是为了控制内存中某个类的实例数量。
懒汉式采用懒加载,时间换空间,因此需要注意获取实例时的并发安全问题,即便正确并发,每次获取实例的时候还是要浪费一次判断;饿汉式空间换时间,在定义单例对象时就完成实例化,因为JVM在初始化一个类的时候(即调用类构造函数<clinit>())会自动同步,因此不用关心线程安全问题,但是一旦完成类加载过程,无论是否使用该单例,该单例都已经实际占用内存;枚举可以做天然的单例,枚举的思想本质就是该类的实例可以穷举,像季节、性别这种实例可以穷举的类型,然而枚举和饿汉式有一样的缺点,只要加载无论是否使用单例,都会占用内存,但是枚举的构造函数通过反射获取到以后再newInstance是非法的(见例一),因此枚举实现的单例相较之懒汉式和饿汉式,无需在私有的构造函数中再进行单例的判断从而控制构造函数被非法反射调用,即在私有构造函数中省略了if(instance != null){抛异常}。
至于单例类是否需要对反序列化进行控制的问题,一般单例类都是作为工具类来使用,不需要序列化,因此不需要实现java.io.Serializable接口;特殊情况下,如果单例类实现了序列化接口,只需要再readResolve方法中返回单例即可。
静态内部类实现单例,一是解决了懒加载线程安全问题(类加载的三个步骤加载、链接、初始化,即调用类构造函数<clinit>()初始化时JVM会自动同步)和获取单例时的判断问题;二是解决了饿汉式和枚举在加载时无论是否使用就分配内存的问题;三是可以和懒加载、饿汉式一样通过在私有构造中判断单例是否为null来进行非法构造方法反射的控制。
因此,静态内部类来实现单例,是相对较好的一种方式。需要提醒的是,本文是想深入讨论为什么性能好,在实际写项目的时候,大可不必吹毛求疵的追逐性能。
例一 枚举构造函数反射获取后调用newInstance非法
package cn.okc.demo;
public enum Gender {
MALE, FEMALE;
}
package cn.okc.demo;
import java.lang.reflect.Constructor;
public class TestGender {
public static void main(String[] args) throws Exception {
Class<Gender> clazz = Gender.class;
@SuppressWarnings("unchecked")
Constructor<Gender>[] constructors = (Constructor<Gender>[]) clazz.getDeclaredConstructors();
for (Constructor<Gender> c : constructors)
System.out.println(c);
Constructor<Gender> constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Gender gender = constructor.newInstance("MALE", 0);
System.out.println(gender);
}
}
private cn.okc.demo.Gender(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at cn.wxy.demo.TestGender.main(TestGender.java:15)
二、静态内部类实现单例及延迟加载验证测试
例二 静态内部类实现单例示例代码
package cn.okc.demo;
public class Singleton {
// 静态内部类实现单例
private static class Inner {
// 单例对象
private static Singleton singleton = new Singleton();
// 类加载分为加载、链接、初始化三大步骤
// 其中链接又分为验证、准备和解析三小个步骤
// 类中静态的内容在编译阶段都会被编译到类构造函数<clinit>()中,在初始化步骤调用
// 因此这个代码块的调用标志着内部类被初始化了
static {
System.out.println("内部类被解析了");
}
}
// 私有化构造函数
private Singleton() {
// 判断单例对象是否已经存在,用于控制非法反射单例类的构造函数
if (Inner.singleton != null)
try {
throw new IllegalAccessException("单例对象已经被实例化,请不要非法反射构造函数");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
// 合法获取单例对象的途径
public static Singleton getInstance() {
return Inner.singleton;
}
}
例三 延迟加载测试(HotSpot)
-----------------------------------------------------------------------------------------------------------------------------
如例三所示,外部类被成功加载并初始化,此时并未导致内部类也跟着被初始化,如果内部类被初始化,则内部类的静态块会被执行并输出。
三、详解
为什么静态内部类单例可以实现延迟加载?实际上是外部类被加载时内部类并不需要立即加载内部类,内部类不被加载则不需要进行类初始化,因此单例对象在外部类被加载了以后不占用内存。
实际上,无论是外部类还是静态内部类,对JVM而言,他们是平等的两个InstanceClass对象,只存在访问修饰符限制访问权限的问题,不存在谁包含谁的问题。
从字节码来窥探静态内部类单例延迟加载,需要从类加载的时机和字节码常量池解析的时机两个方面来得到答案。
1. 窥探字节码
例四 外部类字节码
Classfile /D:/dev_code/workspace_neon/jdbc/target/classes/cn/okc/demo/Singleton.class
Last modified 2016-10-20; size 856 bytes
MD5 checksum 845fd5779231adacc4cebe26f2515a66
Compiled from "Singleton.java"
public class cn.wxy.demo.Singleton
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // cn/wxy/demo/Singleton
#2 = Utf8 cn/wxy/demo/Singleton
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Methodref #11.#13 // cn/wxy/demo/Singleton$Inner.access$0:()Lcn/wxy/demo/Singleton;
#11 = Class #12 // cn/wxy/demo/Singleton$Inner
#12 = Utf8 cn/wxy/demo/Singleton$Inner
#13 = NameAndType #14:#15 // access$0:()Lcn/wxy/demo/Singleton;
#14 = Utf8 access$0
#15 = Utf8 ()Lcn/wxy/demo/Singleton;
#16 = Class #17 // java/lang/IllegalAccessException
#17 = Utf8 java/lang/IllegalAccessException
#18 = String #19 // 单例对象已经被实例化,请不要非法反射构造函数
#19 = Utf8 单例对象已经被实例化,请不要非法反射构造函数
#20 = Methodref #16.#21 // java/lang/IllegalAccessException."<init>":(Ljava/lang/String;)V
#21 = NameAndType #5:#22 // "<init>":(Ljava/lang/String;)V
#22 = Utf8 (Ljava/lang/String;)V
#23 = Methodref #16.#24 // java/lang/IllegalAccessException.printStackTrace:()V
#24 = NameAndType #25:#6 // printStackTrace:()V
#25 = Utf8 printStackTrace
#26 = Utf8 LineNumberTable
#27 = Utf8 LocalVariableTable
#28 = Utf8 this
#29 = Utf8 Lcn/wxy/demo/Singleton;
#30 = Utf8 e
#31 = Utf8 Ljava/lang/IllegalAccessException;
#32 = Utf8 StackMapTable
#33 = Utf8 getInstance
#34 = Utf8 (Lcn/wxy/demo/Singleton;)V
#35 = Methodref #1.#9 // cn/wxy/demo/Singleton."<init>":()V
#36 = Utf8 SourceFile
#37 = Utf8 Singleton.java
#38 = Utf8 InnerClasses
#39 = Utf8 Inner
{
public static cn.wxy.demo.Singleton getInstance();
descriptor: ()Lcn/wxy/demo/Singleton;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: invokestatic #10 // Method cn/wxy/demo/Singleton$Inner.access$0:()Lcn/wxy/demo/Singleton;
3: areturn
LineNumberTable:
line 30: 0
LocalVariableTable:
Start Length Slot Name Signature
cn.wxy.demo.Singleton(cn.wxy.demo.Singleton);
descriptor: (Lcn/wxy/demo/Singleton;)V
flags: ACC_SYNTHETIC
Code:
stack=1, locals=2, args_size=2
0: aload_0
1: invokespecial #35 // Method "<init>":()V
4: return
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
}
SourceFile: "Singleton.java"
例五 静态内部类字节码
Classfile /D:/dev_code/workspace_neon/jdbc/target/classes/cn/wxy/demo/Singleton$Inner.class
Last modified 2016-10-20; size 772 bytes
MD5 checksum 87afaa7bf2981e8a99d143ee3e01054f
Compiled from "Singleton.java"
class cn.wxy.demo.Singleton$Inner
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Class #2 // cn/wxy/demo/Singleton$Inner
#2 = Utf8 cn/wxy/demo/Singleton$Inner
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 singleton
#6 = Utf8 Lcn/wxy/demo/Singleton;
#7 = Utf8 <clinit>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Class #11 // cn/wxy/demo/Singleton
#11 = Utf8 cn/wxy/demo/Singleton
#12 = Methodref #10.#13 // cn/wxy/demo/Singleton."<init>":(Lcn/wxy/demo/Singleton;)V
#13 = NameAndType #14:#15 // "<init>":(Lcn/wxy/demo/Singleton;)V
#14 = Utf8 <init>
#15 = Utf8 (Lcn/wxy/demo/Singleton;)V
#16 = Fieldref #1.#17 // cn/wxy/demo/Singleton$Inner.singleton:Lcn/wxy/demo/Singleton;
#17 = NameAndType #5:#6 // singleton:Lcn/wxy/demo/Singleton;
#18 = Fieldref #19.#21 // java/lang/System.out:Ljava/io/PrintStream;
#19 = Class #20 // java/lang/System
#20 = Utf8 java/lang/System
#21 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = String #25 // 被解析了
#25 = Utf8 被解析了
#26 = Methodref #27.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#27 = Class #28 // java/io/PrintStream
#28 = Utf8 java/io/PrintStream
#29 = NameAndType #30:#31 // println:(Ljava/lang/String;)V
#30 = Utf8 println
#31 = Utf8 (Ljava/lang/String;)V
#32 = Utf8 LineNumberTable
#33 = Utf8 LocalVariableTable
#34 = Methodref #3.#35 // java/lang/Object."<init>":()V
#35 = NameAndType #14:#8 // "<init>":()V
#36 = Utf8 this
#37 = Utf8 Lcn/wxy/demo/Singleton$Inner;
#38 = Utf8 access$0
#39 = Utf8 ()Lcn/wxy/demo/Singleton;
#40 = Utf8 SourceFile
#41 = Utf8 Singleton.java
#42 = Utf8 InnerClasses
#43 = Utf8 Inner
{
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #10 // class cn/wxy/demo/Singleton
3: dup
4: aconst_null
5: invokespecial #12 // Method cn/wxy/demo/Singleton."<init>":(Lcn/wxy/demo/Singleton;)V
8: putstatic #16 // Field singleton:Lcn/wxy/demo/Singleton;
11: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
14: ldc #24 // String 被解析了
16: invokevirtual #26 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: return
LineNumberTable:
line 7: 0
line 13: 11
line 14: 19
LocalVariableTable:
Start Length Slot Name Signature
static cn.wxy.demo.Singleton access$0();
descriptor: ()Lcn/wxy/demo/Singleton;
flags: ACC_STATIC, ACC_SYNTHETIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #16 // Field singleton:Lcn/wxy/demo/Singleton;
3: areturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
}
SourceFile: "Singleton.java"
2. 类加载的时机和字节码常量池解析的时机
弄清楚什么时候加载一个类,才能弄清楚什么时候静态内部类会被加载。
这部分参考《深入理解java虚拟机》第七章和《java虚拟机规范》内容,其实主要关注例四外部类字节码第19行常量池中Methodref从符号引用解析成直接引用的时机,这个时机可以验证实在运行时,而不是在加载过程中;第55行,invokestatic(这个方法会导致类加载)调用内部类自动生成的方法例五75行access$0(见77行访问标识符ACC_SYNTHETIC,这个标识符表示内容不在原文件中,而是由虚拟机生成),但是这里要关注的是方法在运行被调用才会生成方法栈(参看《深入理解java虚拟机》第八章内容)。
简而言之:加载的时候方法不会被调用,不会触发外部类getInstance方法中invokestatic指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池——其中类加载的解析步骤又叫常量池解析,主要是将常量池中的符号引用解析成直接引用,但是这个解析过程不一定非得在类加载时完成,可以延迟到运行时进行——这时候和静态内部类有关的Methodref符号解析会延迟到运行时;因此,静态内部类实现单例参会延迟加载。
后续有空再详细补充。。。。。。
四、参考资料
1. 《java虚拟机规范》
2. 《深入理解java虚拟机》
3. 《研磨设计模式》
4. 《HotSpot实战》
附注:
本文如有错漏,烦请不吝指正,谢谢!