Java 核心技术面试精讲
杨晓峰
前 Oracle 首席工程师
125942 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 44 讲
Java 核心技术面试精讲
15
15
1.0x
00:00/00:00
登录|注册

第10讲 | 如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

有序容器的线程安全版本
线程安全队列(Queue/Deque)
CopyOnWriteArrayList
ConcurrentHashmap
利用非常粗粒度的同步方式
CounterCell的操作基于java.util.concurrent.atomic.LongAdder
使用CAS等操作进行无锁并发操作
Java 8中结构与HashMap相似
早期实现基于分离锁
Collections提供的同步包装器并未真正改进
Hashtable效率低下
使用Unsafe、LongAdder等底层手段进行优化
使用CAS等操作进行无锁并发操作
数据存储利用volatile来保证可见性
lazy-load形式的初始化
不再使用Segment
结构上与HashMap相似
利用不可变对象的机制以改进利用Unsafe提供的底层能力
HashEntry内部使用volatile的value字段保证可见性
分离锁实现
并发包提供的线程安全容器类
同步包装器(Synchronized Wrapper)
提供更全面的工具支持
线程安全实现如Vector、Stack性能不佳
大部分不是线程安全的
产品代码中是否需要使用并发容器
ConcurrentHashMap分析
为什么需要ConcurrentHashMap?
CounterCell的操作基于java.util.concurrent.atomic.LongAdder
Java 8中ConcurrentHashMap的变化
ConcurrentHashMap的高效线程安全实现
保证容器线程安全的方式
并发包(java.util.concurrent)
Java集合框架的典型容器类
一课一练
知识扩展
如何保证集合是线程安全的?

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

我在之前两讲介绍了 Java 集合框架的典型容器类,它们绝大部分都不是线程安全的,仅有的线程安全实现,比如 Vector、Stack,在性能方面也远不尽如人意。幸好 Java 语言提供了并发包(java.util.concurrent),为高度并发需求提供了更加全面的工具支持。
今天我要问你的问题是,如何保证容器是线程安全的?ConcurrentHashMap 如何实现高效地线程安全?

典型回答

Java 提供了不同层面的线程安全支持。在传统集合框架内部,除了 Hashtable 等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。
另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:
各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList。
各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。
各种有序容器的线程安全版本等。
具体保证线程安全的方式,包括有从简单的 synchronize 方式,到基于更加精细化的,比如基于分离锁实现的 ConcurrentHashMap 等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。

考点分析

谈到线程安全和并发,可以说是 Java 面试中必考的考点,我上面给出的回答是一个相对宽泛的总结,而且 ConcurrentHashMap 等并发容器实现也在不断演进,不能一概而论。
如果要深入思考并回答这个问题及其扩展方面,至少需要:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

ConcurrentHashMap是为了解决传统集合框架在高并发情况下性能低下的问题而设计的线程安全容器类。它采用了精细化的线程安全机制,如基于分离锁实现,以提高并发表现。文章详细介绍了ConcurrentHashMap的设计实现和演进过程,包括早期基于分离锁和volatile字段的实现,以及Java 8及之后版本中利用CAS等操作进行无锁并发操作的优化。此外,文章还提到了ConcurrentHashMap的初始化操作和同步逻辑上的优化。总的来说,ConcurrentHashMap通过精细化的线程安全机制和优化的内部实现,有效提高了在高并发情况下的性能表现。同时,文章还介绍了CounterCell的操作基于java.util.concurrent.atomic.LongAdder进行的,是一种JVM利用空间换取更高效率的方法,利用了内部的复杂逻辑。这些技术特点使得ConcurrentHashMap成为在高并发情况下的性能优秀的选择。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 核心技术面试精讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(67)

  • 最新
  • 精选
  • 徐金铎
    需要注意的一点是,1.8以后的锁的颗粒度,是加在链表头上的,这个是个思路上的突破。

    作者回复: 是的

    2018-05-26
    6
    117
  • Sean
    最近用ConcurrentHashMap的场景是,由于系统是一个公共服务,全程异步处理。最后一环节需要http rest主动响应接入系统,于是为了定制化需求,利用netty写了一版异步http clinet。其在缓存tcp链接时用到了。 看到下面有一位朋友说起了自旋锁和偏向锁。 自旋锁个人理解的是cas的一种应用方式。并发包中的原子类是典型的应用。 偏向锁个人理解的是获取锁的优化。在ReentrantLock中用于实现已获取完锁的的线程重入问题。 不知道理解的是否有误差。欢迎指正探讨。谢谢

    作者回复: 正确,互相交流 偏向锁,侧重是低竞争场景的优化,去掉可能不必要的同步

    2018-05-28
    2
    54
  • 虞飞
    老师在课程里讲到同步包装类比较低效,不太适合高并发的场景,那想请教一下老师,在list接口的实现类中。在高并发的场景下,选择哪种实现类比较好?因为ArrayList是线程不安全的,同步包装类又很低效,CopyonwriteArrayList又是以快照的形式来实现的,在频繁写入数据的时候,其实也很低效,那这个类型该怎么选择比较好?

    作者回复: 目前并发list好像就那一个,我觉得不必拘泥于list,不还有queue之类,看场景需要的真是list吗

    2018-05-27
    26
  • Leiy
    我感觉jdk8就相当于把segment分段锁更细粒度了,每个数组元素就是原来一个segment,那并发度就由原来segment数变为数组长度?而且用到了cas乐观锁,所以能支持更高的并发,不知道我这种理解对吗?如果对的话,我就在想,为什么并发大神之前没想到这种,哈哈😄,恳请指正。谢谢

    作者回复: 基本正确,cas只用在部分场景; 事后看容易啊,说比做容易,😄

    2018-05-29
    13
  • coder王
    您说的synchronized被改进很多很多了,那么在我们平常使用中,就用这个synchronized完成一些同步操作是不是OK?😁

    作者回复: 通常是的,前提是JDK版本需要新一点

    2018-05-28
    3
    12
  • 约书亚
    这期内容太难,分寸不好把握 看8的concurenthashmap源码感觉挺困难,网上的博文帮助也不大,尤其是扩容这部分(似乎文章中没提) 求问杨大有没有什么窍门,或者有什么启发性的paper或文章? 可以泛化成,长期对lock free实现多个状态修改的问题比较困惑,希望得到启发

    作者回复: 本文尽量梳理了相对比较容易理解的部分;扩容细节我觉得是个加分项,不是每个人都会在乎那么深入;窍门,可以考虑画图辅助理解,我是比较笨的类型,除了死磕,不会太多窍门……

    2018-05-26
    9
  • mongo
    请教老师:putVal方法中,什么情况下会进入else if ((fh=f.hash) == MOVED)分支?是进行扩容的时候吗?nextTable是做什么用的?

    作者回复: 我理解是的,判断是个ForwardingNode,resize正在进行; nexttable是扩容时的临时过渡

    2018-05-26
    5
  • mongo
    请教老师:putVal方法的第二个if分支,为什么要用tabAt?我的认识里直接数组下标寻址tab[i=(n-1) & hash]也是一个原子操作,不是吗?tabAt里面的getObjectVolatle()方法跟直接用数组下标tab[i=(n-1) & hash]寻址有什么区别?

    作者回复: 这个有volatile load语义

    2018-05-26
    2
    5
  • Xg huang
    这里有个地方想跟老师交流一下想法, 从文中"所以,ConcurrentHashMap 的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数 2),来试图获得可靠值。如果没有监控到发生变化(通过对比 Segment.modCount),就直接返回,否则获取锁进行操作。" 可以看出, 在高并发的情况下, "size()" 方法只是返回"近似值", 而我的问题是: 既然只是一个近似值, 为啥要用这种"重试,分段锁" 的复杂做法去计算这个值? 直接在不加锁的情况下返回segment 的size 岂不是更简单? 我能理解jdk开发者想尽一切努力在高性能地返回最精确的数值, 但这个"精确" 度无法量化啊, 对于调用方来说,这个值依然是不可靠的啊. 所以, 在我看来,这种做法收益很小(可能是我也比较懒吧), 或者有些设计上的要点我没有领悟出来, 希望老师指点一下.

    作者回复: 这个是在代价可接受情况下,尽量准确,就像含金量90%和99.9%,99.999%,还是有区别的,虽然不是百分百

    2018-06-08
    4
  • 行者
    老师麻烦讲讲自旋锁,偏向锁的特点和区别吧,一直不太清楚。

    作者回复: 好,后面有章节

    2018-05-27
    3
收起评论
显示
设置
留言
67
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部