美文网首页
从一个经典案例分析线程可见性

从一个经典案例分析线程可见性

作者: dsjaikdnsajdnua | 来源:发表于2022-11-03 16:01 被阅读0次

    从一个经典案例分析线程可见性

    为什么需要深入底层?

    1. 机械同感(来源于赛车,越清楚赛车的结构就越能发挥出赛车的性能)

    举例mysql B+树-----------为什么like %在 左边就没办法使用索引、为什么日期格式化了就用不了索引、为什么联合索引会断(介绍索引桥)、为什么innodb 适合95%的情况

    不懂原理的知识就像是漂浮在空中的气球,很容易被吹走。

    1. 可以装逼

    原始代码

    **VisibilityTest**,理想的效果:启动线程A,一直count,然后启动线程B去停止线程A的While循环
    
    思考

    为什么不可以??因为线程安全问题。那么为什么会有这个线程安全的问题???

    如何让线程B干预线程A??

    使用volatile

    为什么可以?volatile语义-保证了可见性

    • 写:当写一个volatile修饰得变量,JMM会把该线程对应的共享变量刷新到主存中:
    • 读:当读一个volatile修饰得变量,JMM会把该线程对应的本地内存设置为无效,线程直接读取主存中的变量:
    思考?

    在我们多个服务之间,是如何同步数据的,等类似的场景??

    是否会在本地维护一份数据副本,以提高性能。

    1. 使用同一个地方存储
    2. 消息队列

    其实宏观世界是如此,微观世界也是一样的。

    插入---JMM模型简介

    CPU模型

    为了彻底压榨CPU性能,引入高速缓存L1,L2,L3(L1,L2是核心特有的,每颗核心都有。但是L3是核心共享的)查找顺序:L1--L2----->L3----->内存---->硬盘

    image-20221103175153987.png

    JMM模型

    image-20221103151515169.png

    所以为什么可以???

    因为:FULL_MEM_BARRIER(内存屏障) bytecodeInterpreter.cpp >>>>>>> orderAccess_linux_x86(只看linux x86 cpu下的实现) 【思考------单核CPU是否存在以上问题???】 >>>>> fence实现。

    本质:lock; addl $0,0(%%esp) 这个lock前缀指令,会等待它之前得所有指令完成,并把缓存所有得写操作写回内存,之后开始执行,,并且因为缓存一致性协议,刷新缓存(sotre buffer)得操作会导致其他得cache副本失效。

    使用System.out.println(count);

    为什么可以?synchronized,这也就是为什么snor不允许使用sout的原因,性能低。

    那么为什么synchronized可以让共享变量变得让线程之间可见??

    因为:FULL_MEM_BARRIER(内存屏障) objectMonitor.cpp --->

    JDK的差异演示

    为什么client可以?因为client一般认为不追求性能。

    详细的分析过程:JDK Client & Server 变量可见性问题 - 简书 (jianshu.com)

    我认为,Server模式比Client 的变量做了更多性能的优化,比如这个成员变量。Server为了更高的性能,貌似不会保证成员变量在多线程环境下的一致性,需要指明:volatile才能保证多线程环境下的一致。(多线程在不同的核心进行计算,变量是在寄存器中参与运算的,不同核心运算(多线程下)之间的运算会有变量的副本,这就导致了不同线程实际上操作的变量不是同一份,这就是线程不安全,但是把寄存器的变量刷回主存,同时还需要对其他线程加锁才行,性能比较低,所以Server就默认采用了"牺牲一致性,保证性能"的方案把,Server需要手动添加volatile保证线程安全。目测Client 版本牺牲了性能保证了一致性,所以多线程操作的相当于操作同一份变量。)

    使用Integer

    为什么可以??查看Integer源码---->本质是一个final 类型的int ,final 的语义,代表着不可变,就意味着,也有内存屏障(全屏障)

    编译器会在final域的写之后,插入一个StoreStore屏障.编译器会在读final域操作的前面插入一个LoadLoad屏障

    查看Integer的自动装箱拆箱

    (2条消息) Integer类自动拆箱,装箱解析_温JZ的博客-CSDN博客_integer自动拆箱

    在count上面加volatile

    为什么可以??

    读:当读一个volatile修饰得变量,JMM会把该线程对应的本地内存设置为无效,线程直接读取主存中的变量:

    思考:什么是内存屏障??

    插入概念:指令重排。多核之间的指令重排没办法保证。

    写内存屏障:写的数据一定要写入到主内存!让其他线程可见。

    读内存屏障:强制让缓存失效,去读取主内存的内容,这样其他线程修改的数据就可见了。

    思考:以上都是本质都是通过内存屏障(MEM_BARRIER)实现的可见性,那么还有没有其他方式去实现??

    思考:既然是CPU内部的是缓存,那么会不会有淘汰时间??

    使用长时间停顿(大于9000ns)

    VisibilityTestLongSleep
    

    使用短时间停顿(小于9000ns)

    VisibilityTestShortSleep
    



    通过上下文切换

    通过Thread.yield

    VisibilityTestYield
    

    Thread.yield 就是让出时间片

    通过Thread.sleep

    VisibilityTestThreadSleep

    总结

    线程安全本质就是对内存使用的安全。

    单核CPU下不存在线程安全不安全的问题。那思考??为什么不把CPU的核心做的很大,直接单核就完事了??

    相关文章

      网友评论

          本文标题:从一个经典案例分析线程可见性

          本文链接:https://www.haomeiwen.com/subject/idqutdtx.html