12

打工人!肝了这套多线程吧!壹

 3 years ago
source link: https://segmentfault.com/a/1190000038303697
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

开篇闲扯

一年又一年,年年多线程。不论你是什么程序员,都逃脱不了多线程并发的魔爪。因为它从盘古开天辟地的时候就有了,就是在计算机中对现实世界的一种抽象。因此,放轻松别害怕,肝了这系列的多线程文章,差不多能吊打面试官了(可别真动手...)。

并发症

并发问题,曾经在单核单线程的机器上是不存在的(不是不想,是做不到)。假如把计算机看成一个木桶,那么跟我们Java开发人员关系最大的就是CPU、内存、IO设备。这三块木板发展至今,彼此之间也形成了较大的性能差异。CPU的核心数线程数在不断增多,内存的速度却跟不上CPU的步伐,同理IO设备也没能跟上内存的步伐。于是就加缓存,经过科学论证三级缓存最靠谱,于是就有了常见的 CPU三级缓存 。然后前辈们再对操作系统做各类调度层面的深度优化,通过软硬兼施的手法,使得软件与硬件的完美结合,才有如今繁荣的互联网。而我们不过是在这座城市里的打工人罢了。

言归正传,本文将分别说明在并发世界里的“三宗罪”: 可见性原子性有序性

罪状一:可见性

前文中有说到CPU的发展经历了从单核单线程到现在的多核心多线程,而内存的读写性能却供应不上CPU的处理能力,于是就增加了缓存,至于前文中提到的三级缓存为什么是三级,不在本文讨论范围,有兴趣自己看去。。。

为什么会有可见性问题?

在单核心时代,所有的线程都是交给一个CPU串行执行,因此不论有多少线程都是排队执行,也就不会形成线程A与B同时竞争target变量的竞争状态,如图一。

QB736jJ.png!mobile

来到多核心多线程时代,每颗CPU都有各自的缓存,如果多个线程分别在不同的CPU上运行,且需要同时操作同一个数据。而每颗CPU在处理内存中的数据时,会先将目标数据缓存到CPU缓存中。这时候CPU们各干各的,也不管目标值有没有被其他CPU修改过,自己在缓存中修改后不管三七二十一就写回去,这肯定是不行的啊兄弟...,而这就是我们Java中常说的数据可见性问题,再追根溯源就是:CPU级别的缓存一致性协议。后边文章会详细解释(别问具体时间,问了就是明天)。

可见性问题怎么解决?

这个简单,如果仅仅是解决可见性,那就Volatile关键字用起来(也不是万能的,慎重考虑),它会将共享变量数据从线程工作内存刷新到主存中,而这个关键字的实现基础是Java规范的内存模型,注意,这里要和JVM内存模型区分开,两者是不一样的东西。那么 Java内存模型 又是什么样的,为啥设计这个内存模型,有哪些好处?下篇详细解释!本文就先放一张简单的图:

6B7zeeM.png!mobile

罪状二:原子性

大家都知道CPU的运行时间是分片进行的,可能CPU这段时间在执行我写的if-else,下一时刻由于操作系统的调度当前线程丢失时间片,又执行其他线程任务去了(呸!渣男)。打断了我当前线程的一个或者多个操作流程,这就是原子性被破坏了,也就是多线程无锁情况下的ABA问题。跟我们期望的完全不一样啊,还是看图说话:

E7buIrB.png!mobile

解释一下就是:想要得到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规则?

一段程序中,前面运行后的结果,对后面的操作来说均可见。即:不论怎么编译优化(编译优化的文章以后会写,关注我,全免费)都不能违背这一指导思想。不能忘本

规则名称 解释 程序顺序规则 在一个线程中,按照程序的顺序,前面的操作先发生于后续的操作 volatile变量规则 对volatile变量进行写操作时,要优先发生于对这个变量的读操作,可以理解为禁止指令重排但实际不完全是一个意思 线程start()规则 很好理解,线程的start()操作要优先发生于该线程中的所有操作(要先有鸡才能有蛋) 线程join()规则 线程A调用线程B的join()并成功返回结果时,线程B的任意操作都是先于join()操作的。 管理锁定规则 在java中以Synchronized为例来说就是加解锁操作要成对且解锁操作在加锁之后 对象终结规则 一个对象的初始化完成happen—before它的finalize()方法的开始 传递性 即A操作先于B发生,B先于C发==>A先于C发生

注:文章里所有类似“先于”、“早于”等词并不严谨不能和Happens-Before划等号,只是这样说更好理解,较为准确的含义是:操作结果对后者可见。

其实,总结来说就是JMM、编译器和程序员之间的关系。

JMM对程序员说:你按顺序写,执行结果就是按照你写的顺序执行的,有BUG就是你自己的问题。
程序员:好的,听你的!
JMM对编译器说:你不能随便改变程序员的代码顺序,我都跟他承诺写啥是啥了,别搞错了。
编译器:收到!(可我还是想优化,我不影响你不就行了,这优化我做定了,奥利给!)

于是就有了这些规则,而对于我们CRUD Boy来说都是不可见的,了解一下就OK!

感谢各位看官!

更多文章请扫码关注或微信搜索 Java栈点 公众号!

viInieb.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK