之前的文章里有总结过java的单例怎么写,具体链接
java中的7种单例模式。
经常听到,类的static变量在类加载时就会初始化,于是有了常说的两种单例模式的对比:饿汉式和静态内部类模式。通常的说法是,两种都是支持线程安全的(关于怎么个安全法请看我上面的链接),饿汉式不被推荐是因为会提前初始化,占用一部分内存。
那我们就用代码说话。
根据jvm加载class文件的过程,先是讲class文件流加载到内存,然后验证是否符合class的文件结构,接着生成对应的Class对象丢到方法区。那么,当我们拿到一个类的Class对象时,说明类加载成功了。
Instance.java
public class Instance {
private static Instance instance = new Instance();
private Instance(){
System.out.println("instance be alloc");
}
public static Instance get(){
System.out.println("instance be called");
return instance;
}
}
入口main方法:
public static void main(String[] a){
// 能拿到对应的class类说明已经加载到虚拟机中
Class instanceClass = Instance.class;
System.out.println("Instance load over"+instanceClass!=null);
}
按照“类在加载后就会实例化static变量”的说法,那么“"instance be alloc”一定会打印出来。我们看下结果:
true
很遗憾,即使对象加载了,Instance对象也并没有被实例。接着调用Instance.get()方法:
public class Main {
public static void main(String[] a){
// 能拿到对应的class类说明已经加载到虚拟机中
Class instanceClass = Instance.class;
System.out.println("Instance load over"+instanceClass!=null);
Instance.get();
}
}
结果:
true
instance be alloc
instance be called
是的,当调用Instance.get()的时候,我们的Instance实例化了对象出来。但是请注意, System.out.println(“instance be called”);这行在return instance之前,说明调用时序是get()-》alloc()-》return
以上的过程,验证了类并不是在加载完之后就会实例static变量。那到底什么时候才会初始化呢?
1.当遇到new,getstatic,putstatic或者invokestatic这四条字节码指令的时候,如果该类没有进行
初始化,则需要先初始化.这四条指令对应的是实例化对象,获取一个静态变量,设置一个静态变量(常量
放在常量池中,不会触发),或者调用静态方法的时候.
2.当时候反射包的方法对类进行反射调用的时候
3.当初始化一个类的时候,发现该类的父类还没有进行初始化,则初始其父类
4.当jvm启动的时候,当用户指定执行一个主类(就是包含main的那个类),虚拟机会先初始化这个类.
很明显,上述的例子是因为满足第一条,执行static方法的时候编译器会生成invokestatic指令,这时候instance没有初始化,所以会执行Instance的构造方法,然后在return返回。
public class Instance1 {
private static class Holder{
private static Instance1 instance = new Instance1();
}
private Instance1(){
System.out.println("instance1 alloc");
}
public static Instance1 get(){
System.out.println("instance1 called");
//特意不返回instance
return null;
}
}
注意,在get方法特意返回了null.
public class Main {
public static void main(String[] a){
// 能拿到对应的class类说明已经加载到虚拟机中
Class instanceClass = Instance1.class;
System.out.println("Instance load over"+instanceClass!=null);
Instance1.get();
}
}
执行结果:
true
instance1 called
发现Instance1的构造方法并没有被调用。让get()返回instance实例再打印:
public class Instance1 {
private static class Holder{
private static Instance1 instance = new Instance1();
}
private Instance1(){
System.out.println("instance1 alloc");
}
public static Instance1 get(){
System.out.println("instance1 called");
return Holder.instance;
}
}
结果是:
true
instance1 called
instance1 alloc
这下总算实例化了!但是!强调出来,顺序是get()-》print-》alloc()-》return
从上面讲到的初始化的时机来印证,当执行get()方法时,Instance类并没有静态变量,当执行Hoder.instance的时候,是触发了对instance静态变量的引用,编译时生成的invokestatic(内部类对外部类私有变量的引用,见之前的文章java 内类和外类的关系)触发,使得instance作为Holder的静态变量被初始化。
比较两个过程:
饿汉式:get-》alloc-》return
静态内部类:get-》print(或者其他的方法体)-》alloc-》return
发现后者是多了一个过程,那就是在get()和return之间的代码执行。
首先,类加载的时候就会初始化静态变量,这是不准确的;其次静态内部类式确实比饿汉式晚实例化,但是仅限于get和return之间的代码块,如果这部分代码块很简单甚至没有,那么两者的差距微乎其微。
如果只是写一个简单单例,两者几乎不存在什么性能上的差异。