01 | 使用了并发工具类库,线程安全就高枕无忧了吗?
01 | 使用了并发工具类库,线程安全就高枕无忧了吗?
讲述:王少泽
时长18:29大小14.81M
没有意识到线程重用导致用户信息错乱的 Bug
使用了线程安全的并发工具,并不代表解决了所有线程安全问题
没有充分了解并发工具的特性,从而无法发挥其威力
没有认清并发工具的使用场景,因而导致性能问题
重点回顾
思考与讨论
赞 112
提建议
精选留言(105)
- broccoli置顶2020-03-10尝试回答一下思考题: - 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进去)
共 8 条评论97 - 何岸康置顶2020-03-16问题一:不可以。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,不会写入。展开
作者回复: 非常完美的回答
62 - Wiggle Wiggle置顶2020-03-10关于 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);
15 - le2020-03-09我有一点不太明白,那ThreadLocal的意义呢? 难得是在特定情况下?如:没有用线程池?或者是不想写参数传递值? 用ThreadLocal 从controller传递到dao中 一个请求结束之前给他把值 清空吗(小白一个...求大佬解答)
作者回复: controller向dao传值没有必要,ThreadLocal可以理解为绑定到线程的Map,相同线程的不同逻辑需要共享数据(但又无法通过传值来共享数据),或为了避免相同线程重复创建对象希望重用数据,可以考虑使用ThreadLocal
共 8 条评论27 - pedro2020-03-10第一节就已经收获颇丰了,吾尝终日而思矣,不如须臾之所学也。25
- 汝林外史2020-03-09老师的文章真的是最贴近开发实际,绝对超值。看您代码中都是用的lambda表达式,我工作中都不知道怎么应用,请问老师针对lambda表达式应该怎么深入学习呢?
作者回复: 专栏会有一篇加餐来介绍
共 8 条评论18 - Darren2020-03-10试着回答下问题: 1、ThreadLocalRandom,不能使用静态变量,因为在初始化的时候,通过Unsafe把seed和当前线程绑定了,在多线程情况下,只有主线程和seed绑定了,其他线程在获取seed的时候就是有问题的; 2、computeIfAbsent的value是接受一个Function,而putIfAbsent是是接受一个具体的value,所以computeIfAbsent的使用应该是非常灵活的。展开
作者回复: 👍🏻
共 2 条评论17 - Daizl2020-03-16老师,一般而言并发工具包括同步器和容器两大类,这2大类没太明白怎么区分的。
作者回复: 举例: 容器:ConcurrentHashMap、ConcurrentSkipListMap、CopyOnWriteArrayList、ConcurrentSkipListSet 同步器:CountDownLatch、Semaphore、CyclicBarrier、Phaser、Exchanger
16 - 编程界的小学生2020-03-09看完这篇文章才恍然大悟ThreadLocal内存泄露原来是线程池线程复用导致的。共 3 条评论10
- Jialin2020-03-09问题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
共 3 条评论9 - L.2020-03-15老师您好,ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。能否替小白通俗的解释下 怎么理解这句话的原子性与线程安全?谢谢。
作者回复: 线程安全是指多线程访问的操作ConcurrentHashMap,并不会出现状态不一致,数据错乱,异常等问题。 原子性在于两个方面: 第一,ConcurrentHashMap提供的那些针对单一Key读写的API可以认为是线程安全的,但是诸如putAll这种涉及到多个Key的操作,并发读取可能无法确保读取到完整的数据。 第二,ConcurrentHashMap只能确保提供的API是线程安全的,但是使用者组合使用多个API,ConcurrentHashMap无法从内部确保使用过程中的状态一致。
8 - hellojd2020-03-11ThreadLocalRandom 的使用场景是啥?第一次听说。感觉是为了解决random随机数生成的线程安全问题。线程间传值用TheadLocal就够了
作者回复: 为了性能,Random用到了compareAndSet + synchronized来解决线程安全问题,虽然可以使用ThreadLocal<Random>来避免竞争,但是无法避免synchronized/compareAndSet带来的开销。考虑到性能还是建议替换使用ThreadLocalRandom(有3倍以上提升),这不是ThreadLocal包装后的Random,而是真正的使用ThreadLocal机制重新实现的Random。
8 - 若镜O2020-03-09super实战性 ,多谢老师的精心整理..
作者回复: 谢谢
共 2 条评论8 - 向前走2020-03-11今天终于知道我们平常没有写Thread,或者线程池,其实它工作在Tomcat的容器下,其实它也是在多线程的环境下,也需要注意多线程下的一些线程安全问题。 老师,我想问下下面的两个问题 1.我们平常在1个方法里,读取mysql的某个表的list数据的时候,在方法里面,用ArrayList来接收,这样会有问题么? 我的理解是首先它在方法里,方法执行时是以栈帧的形式入栈出栈的,栈上面的是线程私有的,所以它是线程安全的,我只是读取,没有修改,那只会不同时候查询出来的数据不一致,可能有新增的数据 2.如果在问题1获取到的数据库列表数据里,进行一些添加和删除操作列表元素,比如我获取到数据库列表的数据后,要加一排合计字段到list中,这样它还是线程安全的么? 是不是只有在类上定义的成员变量,(各种共享资源)如数组,map,list,然后在某个方法里去操作这个共享的集合时才会存在线程安全问题呢 不知道我的理解是不是正确呢,辛苦老师了展开
作者回复: 所谓线程安全问题,只有多线程访问操作共享的资源才会有问题。通常CRUD,获取到的数据,只是局部变量或者是在方法之间串联传递(Controller/Service/Repository),至始至终只有一个线程在操作这些数据,也就是Web服务的工作线程,除非你把这些数据又提及到一个线程池或一个线程去处理。 当这些list是类的字段的时候就要小心了,尤其是当类又是Bean可能成为单例的话就要更小心了,字段被多个线程并发访问都可能有线程安全问题。 业务代码一般都是直肠子的CRUD,并且三层架构都是无状态的,如果Controller/Service是有状态的,或是你使用了线程池做一些异步处理,那么需要小心多线程问题。
7 - Monday2020-03-13lambda看起来真的是起劲。要好好补补课。6
- yihang2020-03-18还是有疑问❓看了Random源码,只有setSeed方法加了synchronized,而它只会在实现类并非Random类时才会调用(139行),那么这同步开销从何而来?只有next方法是用了cas。
作者回复: 嗯,锁开销在这里不是主要问题,并发下ThreadLocalRandom会比ThreadLocal<Random>性能好不少,估计在3到8倍左右,既然性能更高就没必要用后者,至于为什么前者会更快你可以继续研究一下 我想了一下,使用ThreadLocal<Random>其实是两个过程,先是从ThreadLocal获得Random,这个开销其实不少的,而ThreadLocalRandom是直接使用unsafe从thread上去拿信息的,2步变为1步,这个开销不得不考虑
共 4 条评论4 - 刘大明2020-03-14今天早上5点起来,配置老师的环境,实际的跑一遍老师的代码.学到很多.而且也看到了自己很多知识点的欠缺. 1.很多并发知识并不知道怎么用,怎么学呢? 2.怎么像老师一样熟练学lambda表达式. 希望跟着老师专栏学习的同时,也能好好补一下其他的知识点.
作者回复: 1. 所有并发工具全部自己做一遍实验,写一下demo 2. 需要时间,下周的加餐我会再来介绍一下lambda的学习
共 4 条评论4 - 小氘2020-03-09课后思考题: 1 不能。ThreadLocalRandom的用法是每个线程各用各的,官方文档说ThreadLocalRandom.current().nextX(...)这么用就不会导致在多线程之间共享。 2 他们都是原子操作,都会根据key的存在情况做后续操作,putIfAbsent不会对value处理,computeIfAbsent的第二个参数是Function接口可做的更多。
作者回复: 👍🏻
4 - 听雨2020-03-09ConcurrentHashMap怎么保证内部数组元素的可见性呢,我看源码里只用volatile修饰了table,但是volatile也保证不了数组里面元素的可见性呀,还请老师解惑!
作者回复: 看一下tabAt方法,其使用Unsafe的getObjectVolatile直接读取内存数据
4 - syp2020-04-06把所有评论和老师的解答全看了一遍竟然有了更深的理解,正所谓授业解惑👍3