记录一下本人关于java单例模式-饿汉模式的思考
参考文章: 波霸取经-单例模式、 波霸取经-Java类加载基础
背景:在我们android面试中,java设计模式往往是绕不开的话题,其中单例模式的出场次数非常的高,近日我在单例模式诸多写法中发现了一些模糊不清的东西,在此记录一下
今日主人公:表弟
看代码
//懒汉模式
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//饿汉模式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
现在面试官问:请说说这两种单例模式有何不同?
表弟回答:“懒汉模式,在使用时才创建具体实例,速度稍慢,但能避免不必要的资源浪费;饿汉模式,上来就创建了具体实例,使用时速度快,但如果未使用的话会造成不必要的资源浪费。”
面试官又问:能解释一下这句饿汉模式上来就创建具体实例,怎么上来的?怎么创建的?
表弟:“这个。。。额,这不是直接就new出来了嘛?”心想:这显而易见so easy!但是再一想,程序启动后,都没有走到这个Singleton的类里面,会去创建这个单利对象吗?我这个“上来”好像用的不太恰当,上到哪里了?谁上的?上的谁?这饿汉模式的实例到底啥时候创建的啊?大家不都是这么答的嘛?
显而易见,“饿汉模式上来就创建具体实例”这个表述显然是不对的,面试官成功拿下表弟一血
思考:Singleton instance = new Singleton()到底是何时执行的呢?我们往下看:
public static void main(String[] args) {
System.out.println("*** 妹 start ***");
System.out.println(Test.myAge);
// System.out.println(Test.brother.age);
System.out.println("*** 妹 end ***");
}
public class Test {
public static final int myAge = 30;
public static final Brother brother = new Brother();
static {
System.out.print("Test.class initialization");
}
static class Brother {
int age = 26;
}
}
输出:
*** 妹 start ***
30
*** 妹 end ***
可以看到Test类中的static代码块未执行,即Test类未加载,但myAge的值可以成功访问得到,这好像就是表弟说的“上来”就创建了,就有值了。但真的是这样的吗?我们继续看Brother。
public static void main(String[] args) {
System.out.println("*** 妹 start ***");
// System.out.println(Test.myAge);
System.out.println(Test.brother.age);
System.out.println("*** 妹 end ***");
}
输出:
*** 妹 start ***
Test.class initialization
26
*** 妹 end ***
什么?Test类中的static代码块执行了,说明Test类进行了加载,但是刚才访问Test.myAge也没有触发类加载啊,这Brother.age不也应该是“上来”就有了,“上来”就能访问的吗,怎么到了表弟(Brother)这就不行了呢,非要类先初始化才可以访问吗?
这里有几个知识点:
1.public static final int myAge = 30;这个是编译期常量,‘在编译期就被放入常量池,后面访问这个变量都会在常量池找,跟类半毛钱关系都没有,所以不会引起类的初始化’,这个确实是“上来”就有值了。
2.public static final Brother brother = new Brother();这个不是编译期常量,这就是普通的静态常量,它无法在编译期被创建,在外部直接访问brother会先触发 类加载,即,先进行类加载,再创建brother;换句话说,只有先触发了Test类的类加载,brother才能完成初始化,即brother不是“上来”就有值的!
3.什么是编译期常量,什么是类加载,什么情况下会触发类加载,请看这里,讲得很详细,表弟都可以读懂:juejin.cn/post/698476…
到这里,我们回头再看饿汉模式
//饿汉模式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
既然实例对象不是‘上来’就创建了,要getInstance()方法调用了才创建,那么这和‘懒汉模式’有什么区别吗,这‘饿汉’也不饿呀。并非如此,看过上面类加载文章的朋友就会知道,对Singleton.class进行反射,或者访问对Singleton中的静态变量(注意不是编译期常量),都会触发Singleton的类加载,进而初始化Singleton 的实例对象instance,也就是说,饿汉模式的实例对象有可能会在实际使用前就被创建了。
往后,再聊到单例模式时,表弟可以由饿汉模式吹到类加载机制、编译期常量,再回归到更加优雅的 静态内部类单利 ,一套操作下来,秀的面试官头皮发麻,两眼冒光,双膝跪地,泪流满面,泣不成声,双手递上Offer:“你就是我们要寻找的人!”。