JavaGuide
JavaGuide copied to clipboard
懒汉式和饿汉式单例模式并没有很大区别
先把2中单例模式的代码贴上:
饿汉式:
懒汉式:
懒汉式的单例初始化时间在第一次调用的时候。
然后关于饿汉式的初始化时间,网上大部分传闻说: “饿汉式单例在类加载阶段就已经初始化了,典型的空间换时间...”。
首先痛斥下以讹传讹的前辈们,这并不是开发者的精神。
类的生命周期我给作者提过issue,再复习一下:加载,验证,准备,解析,初始化,使用,卸载。 我想问问前辈们,饿汉式的单例在以上哪个阶段被初始化了?哪里占内存空间了?
以我个人的理解:最多在准备阶段,会给类的静态字段的变量赋零值,引用类型为null。
然后就是那8个初始化时机会初始化饿汉式的单例。而8个时机里面,正常使用,只有第一次获取单例的时候才会初始化单例。
所以我说 饿汉式与懒汉式基本无二。
我理解的是饿汉式和懒汉式区别点在于懒汉式在我们的单例对象没有被使用的时候不会将其加载进内,仅此而已,个人觉得两者还是很大区别的。如果说没有区别的话,可能会让很多人有误解。
老哥可能还是没明白我说的意思,老哥之见:懒汉式在类加载阶段不会初始化单例。敢问一句,难道饿汉式会在类加载阶段初始化单例吗?当然不会。
我上面说了那么多,其实总结一句话就是: 懒汉式和饿汉式的单例都只会在遇到new,putstatic,getstatic等指令时初始化单例。
//eager属性只在类初始化时才会被初始化
private static final Eager eager = new Eager();
以上。
我提出这个issue的本意是希望各位同学不要盲目随大流,忽略单例模式类加载和初始化的时机。 懒汉式较饿汉式实现复杂,充其量当个面试题吧。。。 还不如饿汉式实现简单。
@guang19 DCL这玩意就是个面试题,考察知识点而已(很多--)。正常情况下最好使用饿汉模式。
这两个最大的区别其实就是资源的预分配,static 字段应该是在 准备 阶段就把 链接 地址给变量了(不太确定,想要明确答案的话去 jvm 规范里找),所以饿汉模式只要加载过类,他就会初始化static变量(如果后面根本不会使用他也会分配内存),而懒汉模式需要在调用(也就是有需求使用时)才分配内存,而且也有一次并发问题。
注:类加载的方式有很多种,不一定时真正使用时才加载,比如说:Class.forName("");
。
@jinyahuan 请先弄明白加载和初始化的区别,准备阶段是为static字段初始化零值(0,0L,null),初始化阶段才是赋值(执行new Eager()),而第一次调用的时候才会初始化。在我看来,你的说法与网上大部分传闻的说法没啥区别。。
自己看jvm规范,~~static字段是在 解析阶段 设置链接地址的~~。 这段后面补上:static final (基础数据类型变量)在“准备”阶段初始化值,引用类型在“初始化”阶段初始化值。
验证方法上面都说了:Class.forName("");
自己试一下就理解了。
@jinyahuan 你说的解析是将常量池中的符号引用解析为直接引用。
这是 懒汉式反编译后的常量池:
这是饿汉式反编译后的常量池:
你只看到了lazy 和 eager是常量,有符号引用,但并没有看到它们的值。
因为
private static final Eager eager = new Eager();
不同于
private static final String s = "abc";
s 是字面量没错,
lazy 和 eager是常量也没错,但是new Eager() 并不能在类加载阶段就确定 eager 的内存。 只有程序运行时,也就是第一次初始化时才会生成实例。
你所说的 Class.forname() 不过是初始化类8中情况的一种情况而已,Class.forname() 会加载并初始化类。我已经说的很清楚了,
private static final Eager eager = new Eager();
只在初始化类时会执行,那么也就会执行new Eager()。
但我已经说过了,普通情况下,jvm第一次执行getstatic指令才会初始化类,有什么毛病吗?因此我说它们两基本无二又有什么毛病?
我的意思说的很清楚:懒汉式和饿汉式普通使用(不使用其它加载或获取单例的手段)没区别,并且是在1楼就说清楚了。
言尽于此吧,我想我已经说的够清楚了,无需再争论下去了。
学习了,感谢
@guang19 很感谢纠正了我错误的知识点 static final 修饰的引用类型确实是在初始化阶段才执行的。至于懒汉饿汉我也不想多说,懂了就是懂了,不懂的以后可能就懂了。
学习了。
@guang19 驳斥一下你的观点。 你所说的两种单例模式没有区别的证据是两者单例对象生成的时间均为第一次加载的时候,并没有考虑多线程的环境。第二种单例模式的由来主要是因为多线程环境和指令重排的问题。
懒汉模式并没有考虑到多线程下A初始化单例对象后对B的可视化问题,也不能解决new Eager()时先得到索引,后分配空间的问题
类初始化之后还有对象实例化啊,饿汉懒汉应该是指的对象实例化的时机。
@guankang1314 还是觉得题目的意思是两种设计模式实现之间的区别。
@guang19 老哥又要叨扰你下,网上说的“饿汉式单例在类加载阶段就已经初始化了,典型的空间换时间...”确实容易理解成 加载阶段就已经new好了,那肯定不对的。 可以通过下面字节码得知,是在类加载系统的初始化阶段给类变量new的对象:
老哥我觉得你上面说的一些细节也有问题,比如
以我个人的理解:最多在准备阶段,会给类的静态字段的变量赋零值,引用类型为null。
这里准备阶段赋初值是正确的,但是初始化阶段会执行<clinit>
JVM方法, 顺序完成父类子类静态成员变量(类变量)显示初始化和父类子类静态代码块语句。具体请参考#https://github.com/Snailclimb/JavaGuide/issues/941#issuecomment-747338107
欢迎老哥讨论,我觉得网上说的欠严谨但是对于不懂JVM的人来说也对。我觉得也不用这么细抠,其实这句话完善成 "饿汉式单例在类加载系统阶段就已经初始化了" 或者更细一点 "饿汉式单例在类加载的初始化阶段就已经初始化了”。最开始说这话的人应该是想让人区分开饿汉模式在方法执行阶段才new对象,不过确实容易让人理解成 类加载系统的 “加载”阶段。
你说的对,加载的时候是不会执行static代码块进行赋值的,只有链接阶段才会赋值。 ClassLoader().loadClass 就是加载类,可验证。
@guang19 DCL这玩意就是个面试题,考察知识点而已(很多--)。正常情况下最好使用饿汉模式。
这两个最大的区别其实就是资源的预分配,static 字段应该是在 准备 阶段就把 链接 地址给变量了(不太确定,想要明确答案的话去 jvm 规范里找),所以饿汉模式只要加载过类,他就会初始化static变量(如果后面根本不会使用他也会分配内存),而懒汉模式需要在调用(也就是有需求使用时)才分配内存,而且也有一次并发问题。
注:类加载的方式有很多种,不一定时真正使用时才加载,比如说:
Class.forName("");
。
加载是ClassLoader().loadClass,不是Class.forName,Class.forName不只有加载还有链接
饿汉式的eager变量不是在类加载的初始化阶段就会进行初始化吗,而懒汉式的lazy变量要等到调用的时候才会进行初始化?这是两者的区别吧?