在这部分呢,咱们主要讲的是关于java并发的底层实现机制,以及相应的理论原理。此块呢,在《java并发编程的艺术》里,属于第二三章。其实对于我们小菜鸟来讲,前面一来就整这么理论的东西,没看多久就容易头晕,然后看着看着就不知道他在讲啥了,知识串不起来。因此捏,我就把这块放到后面来讲。在有了前面的编程练习后,我们就来剥开并发的外衣,来看看他们衣服下面是什么……(嘿嘿嘿……)
一.内存模型及语义
本菜在上课的时候,学到这部分,教材是《The Art of Multiprocessor Programming》,就是华中科大翻译的《多处理器编程的艺术》。讲道理,在学到这部分的时候,本菜都不知道这部分在讲啥,什么happen before,什么顺序一致性,这都是啥,为啥要有这个东西。因此,要学这个,我们首先得知道,这部分内容存在的意义是什么,解决什么样的问题。这样我们才能学懂。
那要说到这块,我们得从很久很久以前说起……在前面,本小菜给同学们说了,我们现在是在玩高端的并发编程。那并发编程,当然就有了并发编程的玩儿法。并发编程,无非就是多线程编程。而多线程关心的最主要的两个问题,就是线程通信和线程同步。也就类似我们前面说的奶茶店例子里,店员之间如何沟通,以及店员之间的操作顺序。
线程通信
对于线程通信,学过操作系统的童鞋们都知道,线程通信会有几种不同的方式。如共享存储区、信号量、消息等等。而java中,主要采用的是共享存储的方式进行通信的。那java里多线程是怎么利用共享存储进行通信的呢?说到这里,就不得不简单提一下java的内存模型。下图就是java的内存模型:
从上图可以看到,JVM内存主要由程序计数器、JVM栈、本地方法栈、共享堆、方法区组成。其中多线程共享部分主要就是那两个骚绿色的部分:堆和方法区。方法区我们主要用来存储已经被虚拟机加载的类的信息、常量、静态变量、编译后的代码、class文件等。而堆,就是我们用于通信的共享存储区(方法区也会有通信吗?)。
其实从上图也可以看到,每个线程,都有自己的本地存储。其实这是一个抽象的概念,他包含了缓存、写缓冲区等等。一般的写操作,流程是线程先写到自己的本地存储里,然后再刷新到主存里。那为什么要多这么一个步骤呢?其实是为了优化读写性能。举个栗子,比如说线程A要写1-100个数。假如每写一个数,都刷新一次内存,那系统开销会很大。因此我可以把这100个都写了,然后统一刷一次到内存,是不是就会快一点呢?
当然,任何事情都有两面性。有好处,就肯定有坏处。缓存带来的坏处最大的就是数据一致性问题。怎么讲?同样举个例子。
本小菜和女票在一起看电视。女票说她想吃瓜子,让小菜给她剥瓜子皮,她直接吃瓜子仁(万恶的资本主义)。迫于淫威,小菜只能给她剥了。拿个盘子放在桌子上用于放瓜子仁。为了方便,小菜剥的时候,每剥一个就放在手心里,等攒够了一定数量,再一起放到盘子里。所以正当小菜还在往手心放瓜子仁,还没放到盘子里的时候,女票大人想吃一点瓜子仁,但是她只看得到桌上的盘子,伸手一抓,发现盘子里还没有瓜子仁。于是……“小菜!!这么久你剥的瓜子呢!!”
基本上就是内存可见性问题的简单描述了。JMM(java内存模型)就制定了一系列方法来通过控制内存可见性来保障数据的一致性。那具体用哪些方法呢?这就是我们后面要说的各种语义啊什么什么的。
这里,我们总结一下。本地存储带来的问题,在并发中会造成内存可见性问题。也就是别的线程看不到最新的数据,或者是读到的还是旧版本的数据。
线程同步
其实线程同步呢,也就是说控制线程的执行顺序。在多线程下,我们希望多个线程能按照我们编码的顺序,按我们希望的执行的顺序去执行。但是,计算机不是这么听话的。为什么呢?敌人就是重排。在具体程序执行的时候,为了进一步优化程序的运行效率,会把程序的执行顺序重新排序一下。从最开始的编译器、到处理器指令、到内存,都存在重排。那么问题就来了。如果都乱序执行了,那肯定也会出现内存可见性问题。举个简单栗子:一个场景:我要先看一下外面有没有下雨,如果下雨,我就看个电影。可能会被排序为我先看电影,然后我再看外面有没有下雨。尴尬了……那怎么办?而且在不同的处理器上,都有不同的重排规则,那就更复杂了。不过别担心,JMM会搞定一切,经过JMM处理后,再不同的平台上、编译器上,都能保证内存可见性。那JMM是怎么做到的呢?听小菜慢慢道来~
二、语义
好了,现在问题变为了我们如何正确控制多个线程的执行顺序,来保证内存可见问题,以及数据的一致性。为了更好滴、形式化滴描述问题,学术界的常用套路就是搞定义……也就是我们所说的各种语义了。一般看到定于小菜就烦,不过定义也不多,我们看一下。主要的定义有happen-before、As-if-serial。
happen-before:从字面上理解,就是在XX之前已经发生了。其实这是一个描述可见性问题的定义。换句话说,就是假如A在B之前,那么当B在执行的时候,就一定会看到A已经执行过了。
As If Serial:这个语义主要是保证正确性。也就是说即使你用多线程并发执行多个操作,但是执行完后,你的执行结果应该和串行执行的效果一样。
OK,就是这么简单。只要保证了上述的两个语义,那么多线程并发运行基本就没什么问题了。其实这里还有一个定义,叫顺序一致性。其实这是一个理想化的参考模型,这个模型主要强调的是立即可见问题,比如假如线程A在给某个变量赋值后,那么其他线程会立即可以看到这个新的修改(类似于我们讲的分布式系统中的强一致性问题)。但是捏,假如严格在每个地方都严格遵守上述的两个规定,那么其实效率是会打折扣的。因此在JMM的具体实现中,在保证程序的正确运行下,会进行适当的调整。
三、内存语义的应用举例
这里,我们可以看一下,在java的具体实现中,我们从语义的角度,来分析java的各种实现(可以用来给面试官装装13)。
1. volatile
2. 锁
3. final
4.单例模式