从一个经典案例分析线程可见性
为什么需要深入底层?
- 机械同感(来源于赛车,越清楚赛车的结构就越能发挥出赛车的性能)
举例mysql B+树-----------为什么like %在 左边就没办法使用索引、为什么日期格式化了就用不了索引、为什么联合索引会断(介绍索引桥)、为什么innodb 适合95%的情况
不懂原理的知识就像是漂浮在空中的气球,很容易被吹走。
可以装逼
原始代码
**VisibilityTest**,理想的效果:启动线程A,一直count,然后启动线程B去停止线程A的While循环
思考
为什么不可以??因为线程安全问题。那么为什么会有这个线程安全的问题???
如何让线程B干预线程A??
使用volatile
为什么可以?volatile语义-保证了可见性
- 写:当写一个volatile修饰得变量,JMM会把该线程对应的共享变量刷新到主存中:
- 读:当读一个volatile修饰得变量,JMM会把该线程对应的本地内存设置为无效,线程直接读取主存中的变量:
思考?
在我们多个服务之间,是如何同步数据的,等类似的场景??
是否会在本地维护一份数据副本,以提高性能。
- 使用同一个地方存储
- 消息队列
其实宏观世界是如此,微观世界也是一样的。
插入---JMM模型简介
CPU模型
为了彻底压榨CPU性能,引入高速缓存L1,L2,L3(L1,L2是核心特有的,每颗核心都有。但是L3是核心共享的)查找顺序:L1--L2----->L3----->内存---->硬盘
image-20221103175153987.pngJMM模型
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的核心做的很大,直接单核就完事了??
网友评论