a型血人的性格
)?如果没听过,那Java多线程编程你肯定不陌生吧。你去随便找一本讲Java多线程编程的书籍,你就会发现,基本上都会讲到Java内存模型。到底什么是Java内存模型呢?它跟多线程又有什么关系呢?
要了解这块知识,最权威的材料莫过于JSR-133(JMM规范)了。不要被JSR-133这个奇怪的单词吓到了。实际上看懂JSR-133并不难,我也是几乎不费力气的就看懂了。但是,尽管JSR-133每个章节每个概念都不难看懂,但是,要真正理解JMM还是不容易的,不信?那你可以拿下面这几个问题自测一下,看是否都能会答上来呢?
透彻的无盲点的理解JMM是需要非常广和深的计算机理论知识的,涉及到计算机体系结构,操作系统,编译原理,JVM虚拟机规范等,我能力有限也不想大牛,所以为了能顺畅的阐述JMM的来龙去脉,对于有些概念进行了假设,猜想,默认,细节简化,做到“不求甚解”只是为了能很短的篇幅内完整的阐述清楚JMM的来龙去脉,并且能让你看懂。不过,尽管有些细节的忽略,但并不影响文章的正确性。
多线程下并发的执行存在race condition的代码,上述的不一致性问题会导致代码执行结果的不确定性(随机性)。
不同体系结构的内存模型是可能有差别的,我们拿其中一种常见的并且简化一下来。处理器为了高效的运行,提高CPU指令执行的效率,引入了两个优化:CPU缓存和指令重排序。
CPU引入缓存的目的主要是解决内存读写速度和CPU处理速度的差别,使CPU对数据的读写不需要等待较慢的内存。
在只有一个CPU的情况下,使用缓存是没有任何问题的。但在多CPU的情况下,每个CPU都会有一个的缓存,但共享内存。这就有问题了。因为每次CPU写操作,都会先将数据写到缓存,然后再择机刷到内存。同样,CPU读操作,也是先从缓存中读取,不命中再从内存捞。
我们知道缓存会有数据一致性问题(不懂自己查查),CPU缓存也不例外,一个CPU写的数据可能不会被另个CPU立刻读到,这就是我们经常提到的可见性问题。
对于一组指令序列,比如A,B,C,D,E...(每个英文代表一条指令),指令的顺序有可能并非是最终执行的顺序,CPU为了提高执行效率(比如,提高缓存的命中率),可能会在执行的时候,只要存在数据依赖的两条指令,执行先后顺序不变(即偏序有序),就可以的对指令序列重排序。
对于单CPU的情况下,这样的指令重排序并没有什么问题,CPU承诺最终执行的结果跟没有重排序的执行结果是一样子的,不然它也不敢这么的瞎搞。但是对于多CPU的情况下,CPU就不做任何承诺了,一个CPU看到的数据更新操作可能跟其预期的(未指令重排序执行的)是不一致的了。
不过,上述两种CPU优化策略(缓存和指令重排序)大部分情况下都不会出问题,只有当两个CPU并行执行存在数据操作冲突的指令(其中有一个写操作)的时候,才会存在问题。不过,CPU是否问题的存在呢?当然不是。
有些处理器直接就在硬件层面上重排序和缓存,所以就不存在说的问题。但大部分CPU并不想为此效率,所以默认将问题丢给了上层代码,但提供了一些基本的指令来支持缓存和指令重排。
在CPU看来,任何高级语言编写的代码最终也都是要编译成一组操作数据的指令序列,CPU最终执行的就是这样一组指令序列。因为并不是所有的处理器都会妥妥的解决好缓存和指令重排带来的问题,像JAVA这样的跨平台语言,就需要在语言层面解决编译之后的指令的在不同处理器上可能存在的缓存和指令重排序问题。
那如何来解决呢?所以JMM规范就顺理成章的诞生了。那又为什么叫Java内存模型呢?那是因为JMM解决的是我们刚刚讲到的硬件内存模型存在的问题,所以起名叫做Java内存模型。
为什么每次提到JMM都要跟多线程联系在一起呢?因为单线程下,程序被给一个CPU执行。而硬件内存模型里面的缓存和指令重排序的问题也只是在多个CPU并行执行并存在冲突的数据操作的时候才会有问题。
也就是说在Java不涉及多线程运行代码的情况下,也就不存在多CPU并行执行代码,也就不存在说的种种问题。所以Java内存模型讲的内容的前提都是在多线程并发执行race condition代码的时候产生的。
一个线程执行run(),当需要停止Server的时候,另一个线程执行stop(),将stoped设置成true。
从代码角度看,貌似没有什么问题,但是将代码翻译成机器指令之后,就会发现stoped值会因为CPU缓存,存在一致性问题。
当一个CPU执行stoped=true指令时 ,会先写入CPU缓存,具体刷新到内存的时间不确定,另一个执行run()指令序列的CPU,有可能之前已经缓存了stoped值在CPU缓存中,另一个CPU即便将stoped设置为了true,此CPU可能也迟迟读不到最新值。
从Java代码的层面来看,就会出现一种奇怪的现象,明明stoped已经设置为true了,server仍然一直运行。
看这段代码,我们预期的ret值应该是2,因为只有flag=true的时候才会执行ret=val+val,而flag设置为true之前val会被设置为1。但实际上,在多线的情况下,会出现ret=0的情况,这就是操作重排序导致的结果,我们来分析一下。
在fun1()中,A, B操作没有依赖关系,如果发生指令重排序,B先于A执行,flag会先被true,然后val还没有被设置为1,这个时候C通过,D计算结果便成了0。
有问题的代码,在编译成指令时就事先通过处理器提供的 指令,将其编译成一组多CPU下执行安全的指令序列。这个显然是不合适的,因为我们事先并不知道程序是否是在多线程下执行,过度的严格的会导致代码执行的效率。
通过提供一些语法关键词,有程序员发现负责代码在多线程并发竞态下的问题,通过这些关键词来控制,做到多线程下执行结果并非不确定。
a) final本身是指常量类型,但在构造函数中使用final会重排序问题,所以重新定义了final对重排序的规则语义。
b) synchronized本身的虽然并未对解决重排序问题,但通过互斥访问共享变量,做到临界区代码串行执行,并且synchronized语义本身定义了每次进入和退出都要同步CPU缓存,所以CPU缓存的数据一致性问题不存在,并且指令重排序也并不影响指令执行的确定性。
c) volatile这个关键词就是纯粹为了解决可见性,重排序等问题产生的。volatile修饰的数据的读写读写操作会让CPU缓存主动跟主存同步,来一致性,另外通过某些指令重排序,可以完美解决指令重排序导致的问题。
网友评论 ()条 查看