Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
52944 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

01 | 使用了并发工具类库,线程安全就高枕无忧了吗?

computeIfAbsent和putIfAbsent方法的区别
使用普通加锁方式ArrayList
大量读的场景
大量写的场景
利用ConcurrentHashMap的原子性方法computeIfAbsent来做复合逻辑操作
整段逻辑加锁
补充操作由10个线程并发进行
在代码的finally代码块中,显式清除ThreadLocal中的数据
区别
是否可以把它的实例设置到静态变量中,在多线程情况下重用呢?
优化方案
问题场景
优化方案
代码修改方案
问题场景
修正方案
其他坑
ConcurrentHashMap
ThreadLocalRandom
没有认清并发工具的使用场景,因而导致性能问题
没有充分了解并发工具的特性,从而无法发挥其威力
使用了线程安全的并发工具,并不代表解决了所有线程安全问题
没有意识到线程重用导致用户信息错乱的Bug
思考与讨论
问题
使用并发工具类库,线程安全就高枕无忧了吗?

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

你好,我是朱晔。作为课程的第一讲,我今天要和你聊聊使用并发工具类库相关的话题。
在代码审核讨论的时候,我们有时会听到有关线程安全和并发工具的一些片面的观点和结论,比如“把 HashMap 改为 ConcurrentHashMap,就可以解决并发问题了呀”“要不我们试试无锁的 CopyOnWriteArrayList 吧,性能更好”。事实上,这些说法都不太准确。
的确,为了方便开发者进行多线程编程,现代编程语言会提供各种并发工具类。但如果我们没有充分了解它们的使用场景、解决的问题,以及最佳实践的话,盲目使用就可能会导致一些坑,小则损失性能,大则无法确保多线程情况下业务逻辑的正确性。
我需要先说明下,这里的并发工具类是指用来解决多线程环境下并发问题的工具类库。一般而言并发工具包括同步器和容器两大类,业务代码中使用并发容器的情况会多一些,我今天分享的例子也会侧重并发容器。
接下来,我们就看看在使用并发工具时,最常遇到哪些坑,以及如何解决、避免这些坑吧。

没有意识到线程重用导致用户信息错乱的 Bug

之前有业务同学和我反馈,在生产上遇到一个诡异的问题,有时获取到的用户信息是别人的。查看代码后,我发现他使用了 ThreadLocal 来缓存获取到的用户信息。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文通过具体案例展示了在多线程编程中使用并发工具可能遇到的问题以及解决方法。作者以ThreadLocal和ConcurrentHashMap为例,指出了在多线程编程中盲目使用并发工具可能导致的隐患,如线程重用导致的用户信息错乱Bug。文章强调了即使没有显式开启多线程,也可能存在线程安全问题。针对使用ConcurrentHashMap时可能出现的问题,文章提出了解决方案,强调了在代码运行完后,显式地去清空设置的数据的重要性。此外,文章还提到了线程池重用线程时可能出现的问题,以及如何修正代码以避免这些问题。文章通过具体案例展示了在多线程编程中需要注意的细节,帮助读者更好地理解并发工具的使用和潜在问题。文章内容通俗易懂,对于开发者来说具有一定的参考价值。文章还强调了充分了解并发工具的特性,以及认清并发工具的使用场景对于发挥其威力和避免性能问题的重要性。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(106)

  • 最新
  • 精选
  • 何岸康
    置顶
    问题一:不可以。ThreadLocalRandom文档里写了Usages of this class should typically be of the form:ThreadLocalRandom.current().nextX(...)} (where X is Int, Long, etc)。 ThreadLocalRandom类中只封装了一些公用的方法,种子存放在各个线程中。 ThreadLocalRandom中存放一个单例的instance,调用current()方法返回这个instance,每个线程首次调用current()方法时,会在各个线程中初始化seed和probe。 nextX()方法会调用nextSeed(),在其中使用各个线程中的种子,计算下一个种子并保存(UNSAFE.getLong(t, SEED) + GAMMA)。 所以,如果使用静态变量,直接调用nextX()方法就跳过了各个线程初始化的步骤,只会在每次调用nextSeed()时来更新种子。 问题二 1.参数不一样,putIfAbsent是值,computeIfAbsent是mappingFunction 2.返回值不一样,putIfAbsent是之前的值,computeIfAbsent是现在的值 3.putIfAbsent可以存入null,computeIfAbsent计算结果是null只会返回null,不会写入。

    作者回复: 非常完美的回答

    2020-03-16
    67
  • broccoli
    置顶
    尝试回答一下思考题: - 1. 先说结论:不可以,结果是除了初始化 ThreadLocalRandom 的主线程获取的随机值是无模式的(调用者不可预测下个返回值,满足我们对伪随机的要求)之外,其他线程获得随机值都不是相互独立的(本质上来说,是因为他们用于生成随机数的种子 seed 的值可预测的,为 i*gamma,其中 i 是当前线程调用随机数生成方法次数,而 gamma 是 ThreadLocalRandom 类的一个 long 静态字段值)。例如,一个有趣的现象是,所有非初始化 ThreadLocalRandom 实例的线程如果调用相同次数的 nextInt() 方法,他们得到的随机数串是完全相同的。 造成这样现象的原因在于,ThreadLocalRandom 类维护了一个类单例字段,线程通过调用 ThreadLocalRandom#current() 方法来获取 ThreadLocalRandom 单例,然后以线程维护的实例字段 threadLocalRandomSeed 为种子生成下一个随机数和下一个种子值。 那么既然是单例模式,为什么多线程共用主线程初始化的实例就会出问题呢。问题就在于 current 方法,线程在调用 current() 方法的时候,会根据用每个线程的 thread 的一个实例字段 threadLocalRandomProbe 是否为 0 来判断是否当前线程实例是否为第一次调用随机数生成方法,从而决定是否要给当前线程初始化一个随机的 threadLocalRandomSeed 种子值。因此,如果其他线程绕过 current 方法直接调用随机数方法,那么它的种子值就是 0, 1*gamma, 2*gamma... 因此也就是可预测的了。 - 2. 两个方法的区别除了其他同学在评论区提出的参数类型不同以及抛出异常类型不同之外,在文中示例选择 CIA 而不选择 PIA 的原因(以及老师为什么点出来的原因)在于他们在面对 absent key值上的区别: - CIA 根据 mappingFunction 返回的值插入键值对,然后返回这个新值 - 而 PIA 是插入 KV 对后,返回 null 值 因此,如果我们将文中的 CIA 替换成 PIA,如果插入的是 absent key 会抛出空指针异常。其实,在我看来文中示例用 PIA 也不是不行,只要改成先 PIA,然后再去 get(key) 获取那个原子类型 long 然后再自增就 ok 了。(不确定对错,还请老师指正) 那么老师为什么没有这么写呢? - 一是每调用一次这些方法都伴随着一次片段锁的获取与释放,显然 PIA 方法性能要差 - (二就是不够优雅,老师嫌字多...)

    作者回复: 说的非常细非常好 computeIfAbsent和putIfAbsent区别是三点: 1、当Key存在的时候,如果Value获取比较昂贵的话,putIfAbsent就白白浪费时间在获取这个昂贵的Value上(这个点特别注意) 2、Key不存在的时候,putIfAbsent返回null,小心空指针,而computeIfAbsent返回计算后的值 3、当Key不存在的时候,putIfAbsent允许put null进去,而computeIfAbsent不能,之后进行containsKey查询是有区别的(当然了,此条针对HashMap,ConcurrentHashMap不允许put null value进去)

    2020-03-10
    8
    102
  • Wiggle Wiggle
    置顶
    关于 ThreadLocalRandom,其目的是为了避免多线程共享 Random 时竟态条件下性能差的问题(我认为关键在于 Random#nextSeed 方法中使用自旋保证线程安全,而自旋在面对高并发时性能差),官方文档上说正确用法是 ThreadLocalRandom.current().nextX(...),但是没说设置为 static 的话会发生什么,我想进一步研究一下,就去看了一下源码,不知道理解对不对,请老师指正:ThreadLocalRandom#nextSeed 方法中用到了 UnSafe,这块我不了解,但是我没有看到任何保证线程安全的代码,如果并发调用的话会导致无法预料的问题。

    作者回复: 基本原理是,current()的时候初始化一个初始化种子到线程,每次nextseed再使用之前的种子生成新的种子: UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA); 如果你通过主线程调用一次current生成一个ThreadLocalRandom的实例保存起来,那么其它线程来获取种子的时候必然取不到初始种子,必须是每一个线程自己用的时候初始化一个种子到线程,你可以在nextSeed设置一个断点看看: UNSAFE.getLong(Thread.currentThread(),SEED);

    2020-03-10
    16
  • le
    我有一点不太明白,那ThreadLocal的意义呢? 难得是在特定情况下?如:没有用线程池?或者是不想写参数传递值? 用ThreadLocal 从controller传递到dao中 一个请求结束之前给他把值 清空吗(小白一个...求大佬解答)

    作者回复: controller向dao传值没有必要,ThreadLocal可以理解为绑定到线程的Map,相同线程的不同逻辑需要共享数据(但又无法通过传值来共享数据),或为了避免相同线程重复创建对象希望重用数据,可以考虑使用ThreadLocal

    2020-03-09
    8
    29
  • Darren
    试着回答下问题: 1、ThreadLocalRandom,不能使用静态变量,因为在初始化的时候,通过Unsafe把seed和当前线程绑定了,在多线程情况下,只有主线程和seed绑定了,其他线程在获取seed的时候就是有问题的; 2、computeIfAbsent的value是接受一个Function,而putIfAbsent是是接受一个具体的value,所以computeIfAbsent的使用应该是非常灵活的。

    作者回复: 👍🏻

    2020-03-10
    2
    18
  • 汝林外史
    老师的文章真的是最贴近开发实际,绝对超值。看您代码中都是用的lambda表达式,我工作中都不知道怎么应用,请问老师针对lambda表达式应该怎么深入学习呢?

    作者回复: 专栏会有一篇加餐来介绍

    2020-03-09
    8
    18
  • Daizl
    老师,一般而言并发工具包括同步器和容器两大类,这2大类没太明白怎么区分的。

    作者回复: 举例: 容器:ConcurrentHashMap、ConcurrentSkipListMap、CopyOnWriteArrayList、ConcurrentSkipListSet 同步器:CountDownLatch、Semaphore、CyclicBarrier、Phaser、Exchanger

    2020-03-16
    2
    16
  • hellojd
    ThreadLocalRandom 的使用场景是啥?第一次听说。感觉是为了解决random随机数生成的线程安全问题。线程间传值用TheadLocal就够了

    作者回复: 为了性能,Random用到了compareAndSet + synchronized来解决线程安全问题,虽然可以使用ThreadLocal<Random>来避免竞争,但是无法避免synchronized/compareAndSet带来的开销。考虑到性能还是建议替换使用ThreadLocalRandom(有3倍以上提升),这不是ThreadLocal包装后的Random,而是真正的使用ThreadLocal机制重新实现的Random。

    2020-03-11
    9
  • Jialin
    问题1:ThreadLocalRandom 是 ThreadLocal 类和 Random 类的组合,ThreadLocal的出现就是为了解决多线程访问一个变量时候需要进行同步的问题,让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步,ThreadLocalRandom的实现也是这个原理,解决了Random类在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足,因此,类似于ThreadLocal,ThreadLocalRandom的实例也可以设置成静态变量。 问题2: public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)此方法首先判断缓存map中是否存在指定key的值,如果不存在,会自动调用mappingFunction(key)计算key的value,然后将key = value放入到缓存Map,如果mappingFunction(key)返回的值为null或抛出异常,则不会有记录存入map。 public V putIfAbsent(K key, V value)此方法如果不存在(新的entry),那么会向map中添加该键值对,并返回null。如果已经存在,那么不会覆盖已有的值,直接返回已经存在的值。 相同点:两者均是指定的key不存在其对应的value时,进行操作,指定的key存在对应的value时,直接返回value。 不同点: 线程安全性:putIfAbsent线程非安全,computeIfAbsent线程安全; 返回值:指定key对应的value不存在时,putIfAbsent进行设置并返回null,computeIfAbsent进行计算并返回新值; 异常类型:putIfAbsent可能抛出NullPointerException,computeIfAbsent除了NullPointerException,还存在IllegalStateException()和RuntimeException异常

    作者回复: 问题1不太对,ThreadLocalRandom的正确使用方式是ThreadLocalRandom.current().nextX(...),不能在多线程之间共享ThreadLocalRandom

    2020-03-09
    3
    9
  • L.
    老师您好,ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。能否替小白通俗的解释下 怎么理解这句话的原子性与线程安全?谢谢。

    作者回复: 线程安全是指多线程访问的操作ConcurrentHashMap,并不会出现状态不一致,数据错乱,异常等问题。 原子性在于两个方面: 第一,ConcurrentHashMap提供的那些针对单一Key读写的API可以认为是线程安全的,但是诸如putAll这种涉及到多个Key的操作,并发读取可能无法确保读取到完整的数据。 第二,ConcurrentHashMap只能确保提供的API是线程安全的,但是使用者组合使用多个API,ConcurrentHashMap无法从内部确保使用过程中的状态一致。

    2020-03-15
    8
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部