代码精进之路
范学雷
前 Oracle 首席软件工程师,Java SE 安全组成员,OpenJDK 评审成员
38234 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
结束语 (1讲)
代码精进之路
15
15
1.0x
00:00/00:00
登录|注册

24 | 黑白灰,理解延迟分配的两面性

延迟初始化的优缺点
使用类的变量
使用局部变量
使用temporaryMap.put()的原因
使用ConcurrentHashMap的原因
synchronized的使用
检查temporaryMap的值
temporaryMap变量的作用
volatile关键词的使用
延迟初始化的实现方法
声明时初始化 vs 需要时初始化
理解延迟初始化的同步代码
延迟初始化的效率分析
类的变量的初始化方案
局部变量的原则
JDK 11对HashMap的改进
JDK 8对ArrayList的改动
延迟初始化
延迟分配的思路
延迟分配的好处
延迟分配的重要性
减少实例的尺寸
减少实例数量
一起来动手
小结
延迟分配
减少内存使用的两个大方向
黑白灰,理解延迟分配的两面性
参考文章

该思维导图由 AI 生成,仅供参考

上一次,我们讨论了减少内存使用的两个大方向,减少实例数量和减少实例的尺寸。如果我们把时间的因素考虑在内,还有一些重要的技术,可以用来减少运行时的实例数量。其中,延迟分配是一个重要的思路。

延迟分配

在前面讨论怎么写声明的时候,为了避免初始化的遗漏或者不必要的代码重复,我们一般建议“声明时就初始化”。但是,如果初始化涉及的计算量比较大,占用的资源比较多或者占用的时间比较长,声明时就初始化的方案可能会占用不必要的资源,甚至成为软件的一个潜在安全问题。
这时候,我们就需要考虑延迟分配的方案了。也就是说,不到需要时候,不占用不必要的资源。
下面,我们通过一个例子来了解下什么是延迟分配,以及延迟分配的好处。
在 Java 核心类中,ArrayList 是一个可调整大小的列表,内部实现使用数组存储数据。它的优点是列表大小可调整,数组结构紧凑。列表大小可以预先确定,并且在大小不经常变化的情况下,ArrayList 要比 LinkedList 节省空间,所以是一个优先选项。
但是,一旦列表大小不能确定,或者列表大小经常变化,ArrayList 的内部数组就需要调整大小,这就需要内部分配新数组,废弃旧数组,并且把旧数组的数据拷贝到新数组。这时候,ArrayList 就不是一个好的选择了。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

延迟分配是一个重要的技术,可以用来减少运行时的实例数量,从而降低内存使用。通过延迟初始化,可以避免不必要的资源占用,提高性能。文章通过讨论ArrayList和HashMap的实现变化,以及延迟初始化的实现方法,阐述了延迟分配的两面性。延迟初始化可以避免内存资源的浪费,但需要考虑CPU效率和多线程安全等问题。总之,对于使用频率高的类的实现,微小的性能改进都可以带来巨大的实用价值。读者应该根据具体情况,具体分析,采用延迟初始化是否可以提高效率,然后再决定使用这种方案是否划算。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《代码精进之路》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(14)

  • 最新
  • 精选
  • yang
    1 通过采用java内存模型,保证多线程场景下共享资源的可见性 2使用局部变量,可以减少主存与线程内存的拷贝次数 3第一次是初始化,第二次是同步局部变量与属性变量的值,保持一致 4第一次检查是为了快速获取对象,第二次检查是为了防止对象未初始化,就是标准的double check 5是为了线程安全,同时高性能,锁范围更小化 6前者是线程安全,后者是非线程安全 7还是减少主存与线程内存值拷贝的开销 个人理解,如有误,敬请指正

    作者回复: volatile的使用,需要一定程度的同步,也就是你说的拷贝开销。减少volatile变量的引用,可以提高效率。 恭喜你,这些Java的难点你掌握的很扎实!

    2019-02-27
    3
    43
  • 梦醒时分
    我的思考: 1.volatile是用来保证变量的可见性的,这样其他线程才能及时看到变量的修改 2.为啥要使用temporaryMap变量,这里没有想明白 3.两次设置temporaryMap变量,目的是双重检查,防止进入同步代码块中,变量已被赋值了 4.同上 5.synchronized的使用是影响性能的,所有在使用它之前,先校验下是否需要进入同步块中 6.ConcurrentHashMap是线程安全的,而HashMap不是线程安全的 7.为啥使用temporaryMap.put不太清楚

    作者回复: 关于temporaryMap的使用,请参考@yang的留言。

    2019-02-27
    6
  • 拉可里啦
    作为类的全局变量而非静态变量,只能被类的实例所拥有,那么只有一个对象再操作这个全局变量,单线程操作这个变量不会有线程安全问题,多个线程同时操作这个变量有线程安全问题,是以对象为单位的。不知道我这样理解对不对,还请老师指点指点。

    作者回复: 是的,是以对象为单位的。 所以,你看Java的代码里,synchronized(this), this指的就是一个具体的对象。同一个类,实例化的对象不同,也不需要同步。

    2019-04-07
    4
  • Linuxer
    请问各位思考题中的volatile修饰后是不是就只能用concurrenthashmap?要不赋值给局部变量后主存和线程内存还是不同步

    作者回复: volatile修饰符和使用concurrent hash map关系不大。volatile修饰的是标志符,不是标志符指向的内容。

    2019-02-28
    3
  • Jxin
    1.类属性的调用和赋值全部走set和get方法。这种非静态且多次赋值的局部变量应该尽量避免。 2.带锁初始化操作应该上移到get方法。至少从函数功能来看,我认为初始化应该是属于get的。 3.我真不喜欢加锁和每次get都做判断,所以了解业务上下文,如果可以,我会直接给该属性做初始化。 3.如果了解完上下文我对这个集合或则散列表的大小能有一定把我,我会尝试给定一个合适的初始化大小。

    作者回复: 👍都是很好的实践经验! 了解适用场景,是高效代码的关键。

    2019-05-28
    2
  • 鱼筱雨
    范老师,我总是有个疑惑,举个例子 Map<String,String> m = new HashMap<>(); 在这段代码中,左边的泛型中会声明具体类型,而右边的泛型中往往是空的,而我在代码开发中右边的泛型都会和左边保持一致,这样做有什么问题吗?哪种更好一点?

    作者回复: 现在推荐使用的模式是声明时指定类型,使用时让编译器来自动匹配。对于上面的代码,也就是左边声明具体类型,而右边使用空指示符。这样做的主要目的是简化编码,避免不必要的失误和检验。

    2019-08-29
    1
  • aguan(^・ェ・^)
    老师,我有一个疑问,思考题的代码,在多线程的情况下,如果第16行用tempHashmap.put,是会出现空指针的。因为cpu指令重排序,当线程1在执行new map的时候,可能cpu先给temp分配内存空间,对象还没实例化,这时候另外一个线程在第一个if的时候发现temp不为空(因为有地址,但地址里并没有实例化对象),接着去执行16行的代码,会出现空指针问题吧

    作者回复: tempHashmap是一个局部变量,不跨越线程。

    2019-03-06
    1
  • 唐名之
    @yang回到第二点 使用局部变量,可以减少主存与线程内存的拷贝次数 这个点还是有点不明白能解释下嘛?
    2019-03-01
    3
  • Jxin
    采用了临时变量,对象是否就失去了可视性和有序性的特性?这样在new操作时,由于虚拟机的编译优化,cpu时间片切换时不是可能会出现空指针报错吗?
    2019-06-10
    1
  • aguan(^・ェ・^)
    恍然大悟,所以局部变量是解决双重检查重排序空指针问题的安全方法👍
    2019-03-07
    1
收起评论
显示
设置
留言
14
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部