开篇闲扯
一年又一年, 年年多线程. 不论你是什么程序员, 都逃脱不了多线程并发的魔爪. 因为它从盘古开天辟地的时候就有了, 就是在计算机中对现实世界的一种抽象. 因此, 放轻松别害怕, 肝了这系列的多线程文章, 差不多能吊打面试官了(可别真动手...).
并发症
并发问题, 曾经在单核单线程的机器上是不存在的(不是不想, 是做不到). 假如把计算机看成一个木桶, 那么跟我们 Java 开发人员关系最大的就是 CPU, 内存, IO 设备. 这三块木板发展至今, 彼此之间也形成了较大的性能差异. CPU 的核心数线程数在不断增多, 内存的速度却跟不上 CPU 的步伐, 同理 IO 设备也没能跟上内存的步伐. 于是就加缓存, 经过科学论证三级缓存最靠谱, 于是就有了常见的 CPU 三级缓存. 然后前辈们再对操作系统做各类调度层面的深度优化, 通过软硬兼施的手法, 使得软件与硬件的完美结合, 才有如今繁荣的互联网. 而我们不过是在这座城市里的打工人罢了. 言归正传, 本文将分别说明在并发世界里的 "三宗罪": 可见性, 原子性, 有序性.
罪状一: 可见性
前文中有说到 CPU 的发展经历了从单核单线程到现在的多核心多线程, 而内存的读写性能却供应不上 CPU 的处理能力, 于是就增加了缓存, 至于前文中提到的三级缓存为什么是三级, 不在本文讨论范围, 有兴趣自己看去...
为什么会有可见性问题?
在单核心时代, 所有的线程都是交给一个 CPU 串行执行, 因此不论有多少线程都是排队执行, 也就不会形成线程 A 与 B 同时竞争 target 变量的竞争状态, 如图一.
image.PNG
来到多核心多线程时代, 每颗 CPU 都有各自的缓存, 如果多个线程分别在不同的 CPU 上运行, 且需要同时操作同一个数据. 而每颗 CPU 在处理内存中的数据时, 会先将目标数据缓存到 CPU 缓存中. 这时候 CPU 们各干各的, 也不管目标值有没有被其他 CPU 修改过, 自己在缓存中修改后不管三七二十一就写回去, 这肯定是不行的啊兄弟..., 而这就是我们 Java 中常说的数据可见性问题, 再追根溯源就是: CPU 级别的缓存一致性协议. 后边文章会详细解释(别问具体时间, 问了就是明天).
可见性问题怎么解决?
这个简单, 如果仅仅是解决可见性, 那就 Volatile 关键字用起来(也不是万能的, 慎重考虑), 它会将共享变量数据从线程工作内存刷新到主存中, 而这个关键字的实现基础是 Java 规范的内存模型, 注意, 这里要和 JVM 内存模型区分开, 两者是不一样的东西. 那么 Java 内存模型又是什么样的, 为啥设计这个内存模型, 有哪些好处? 下篇详细解释! 本文就先放一张简单的图:
image
罪状二: 原子性
大家都知道 CPU 的运行时间是分片进行的, 可能 CPU 这段时间在执行我写的 if-else, 下一时刻由于操作系统的调度当前线程丢失时间片, 又执行其他线程任务去了(呸! 渣男). 打断了我当前线程的一个或者多个操作流程, 这就是原子性被破坏了, 也就是多线程无锁情况下的 ABA 问题. 跟我们期望的完全不一样啊, 还是看图说话:
image
解释一下就是: 想要得到 temp 为 2 的结果, 但是只能得到 1, 原因就是运行 A 线程的 CPU 干别的去了, 而这时候 B 线程所在的 CPU 后发制 A, 抢先完成了 ++ 的操作并写回内存, 但是 A 不知道, 还傻傻的以为它的到的是 temp 的初恋, 又傻傻的写会去, 然后就心态崩了呀! 偷袭~(出错)
罪状三: 有序性
如果说原子性问题是硬件工程师挖的坑(CPU 别切换多好), 那有序性就妥妥的是软件工程师下了老鼠夹子(夸张了啊, 其实都是为了效率). 之所以存在有序性问题, 完全是编译大神们对我们的关爱, 知道我们普通 Coder 对性能的要求是能跑就行.
因此, 在 Java 代码在编译时期动了手脚, 比如说: 锁消除, 锁粗化 (需要进行逃逸性分析等技术手段) 或者是将 A,B 两段操作互换顺序. 但是, 所有的这一切都不能影响源码在单线程执行情况下的最终结果, 即 as-if-serial 语义. 这是个很顶层的协议, 不论是编译器, 运行时状态还是处理器都必须遵守该语义. 这是保证程序正确性的大前提. 当然, 编译器不仅仅要准守 as-if-serial 语义, 还要准守以下八大规则 --Happens-Before 规则(八仙过海各显神通):
什么是 Happens-Before 规则?
一段程序中, 前面运行后的结果, 对后面的操作来说均可见. 即: 不论怎么编译优化 (编译优化的文章以后会写, 关注我, 全免费) 都不能违背这一指导思想. 不能忘本
image
注: 文章里所有类似 "先于","早于" 等词并不严谨不能和 Happens-Before 划等号, 只是这样说更好理解, 较为准确的含义是: 操作结果对后者可见.
其实, 总结来说就是 JMM, 编译器和程序员之间的关系.
JMM 对程序员说: 你按顺序写, 执行结果就是按照你写的顺序执行的, 有 BUG 就是你自己的问题.
程序员: 好的, 听你的!
JMM 对编译器说: 你不能随便改变程序员的代码顺序, 我都跟他承诺写啥是啥了, 别搞错了.
编译器: 收到!(可我还是想优化, 我不影响你不就行了, 这优化我做定了, 奥利给!)
于是就有了这些规则, 而对于我们 CRUD Boy 来说都是不可见的, 了解一下就 OK!
感谢各位看官!
作者: 罗拉快跑跑跑跑
来源: http://www.jianshu.com/p/c24702fee5bd