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

开篇闲扯

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

并发症

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


罪状一:可见性

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

为什么会有可见性问题?

    在单核心时代,所有的线程都是交给一个CPU串行执行,因此不论有多少线程都是排队执行,也就不会形成线程A与B同时竞争target变量的竞争状态,如图一。
打工人!肝了这套多线程吧!壹_第1张图片
    来到多核心多线程时代,每颗CPU都有各自的缓存,如果多个线程分别在不同的CPU上运行,且需要同时操作同一个数据。而每颗CPU在处理内存中的数据时,会先将目标数据缓存到CPU缓存中。这时候CPU们各干各的,也不管目标值有没有被其他CPU修改过,自己在缓存中修改后不管三七二十一就写回去,这肯定是不行的啊兄弟...,而这就是我们Java中常说的数据可见性问题,再追根溯源就是:CPU级别的缓存一致性协议。后边文章会详细解释(别问具体时间,问了就是明天)。

可见性问题怎么解决?

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


罪状二:原子性

    大家都知道CPU的运行时间是分片进行的,可能CPU这段时间在执行我写的if-else,下一时刻由于操作系统的调度当前线程丢失时间片,又执行其他线程任务去了(呸!渣男)。打断了我当前线程的一个或者多个操作流程,这就是原子性被破坏了,也就是多线程无锁情况下的ABA问题。跟我们期望的完全不一样啊,还是看图说话:
打工人!肝了这套多线程吧!壹_第3张图片
    解释一下就是:想要得到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栈点公众号!

打工人!肝了这套多线程吧!壹_第4张图片

你可能感兴趣的:(java)