总体架构
![](https://i-blog.csdnimg.cn/blog_migrate/5a7762f327a5fde0cb558ebe9fe8f5df.png)
之所以画成这样,是因为对《深入理解java虚拟机》中提到的内存模型中内存间的交互操作的理解,而不是简单的像书中的图一样。基于执行引擎和主内存的交互方式,以及read、load,store、write操作的含义,我个人认为应该加一个执行引擎和变量值暂存(有可能是寄存器、缓存等)。
如图所示,每个线程都会有自己的工作内存,在工作时,对共享变量的访问必须先从主内存加载到工作内存,线程对共享变量的读取/赋值等操作都是在工作内存中进行的。对共享变量的修改需要刷新到主内存中才能算做真正完成修改。
另外,注意这里的共享变量是实例变量、类变量、数组元素等,而不是局部变量和方法参数,因为这两种是属于线程私有的,不存在共享问题。
内存间的交互操作
关于主内存和工作内存间的交互协议,java内存模型定义了8种基本操作,注意这不是字节码指令。它们都是原子操作(不考虑long、double的非原子协定)
- lock:加锁,一个线程lock了共享资源,那么这个共享变量就标识为线程独占状态
- unlock:解除共享资源的线程独占状态
- read:作用于主内存的变量,将主内存变量的值加载到工作内存中(图中工作内存变量值),为load操作做准备
- load:作用于工作内存的变量,将read的值放入工作内存变量副本中
- use:作用于工作内存的变量,将变量的值传递给执行引擎,当遇到使用变量的值的字节码指令时会执行这个操作
- assign:作用于工作内存的变量,将执行引擎得到的值放入工作内存的变量副本中,当遇到给变量赋值的字节码指令就会执行这个操作
- store:作用于工作内存的变量,将变量的值放入主内存中(这个时候还没有放入变量,和工作内存中的变量值存放差不多,有可能是寄存器或缓存中)
- write:作用于主内存的变量,将store的值方入变量中
三大特性
原子性
原子性是指操作的不会被其他线程中断,无论线程间是怎样的步调,这个操作都不会被其他线程影响。
可见性
可见性指的是一个线程对共享变量的修改可以立即被其他线程知道。
有序性
如果在本线程内观察,所有操作都是有序的,但是在其他线程看来是无序的。前半句指的是“线程内的串行语义性”,后半句指的是“指令重排序”和“主内存工作内存的同步延迟”带来的影响。
指令重排序
指令重排序类型
指令重排序的根本目的是优化程序,提高执行效率。在java代码的编译执行过程中,指令重排序共分为两大类、三小类。
两大类:编译器重排序和处理器重排序。
三小类:在处理器重排序中分出指令级并行重排序和内存系统重排序
如图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/c074ed35e94d232a46108343881689be.png)
编译器重排序
是编译器在不改变单线程的串行语义的前提下,改变语句执行顺序。JMM的编译器重排序规则可以禁止特定类型的编译器重排序。
指令级并行重排序
指令级并行重排序是处理器优化指令流水线执行的措施。现代处理器的指令执行是流水型的,常见的为五级流水:
取指、译码、计算、访存、回写
![](https://i-blog.csdnimg.cn/blog_migrate/f9fb23e9d1181be7d4e6997fcec9e023.png)
如图,在指令1还没有完成整个操作时,指令2就可以开始进行了,这样可以提高运行速度。然而,这也带来了一些问题,指令流水中断是很麻烦的事情,应该尽量避免。如图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/546c9f8a60b5aa58cb7fd33cf4708478.png)
如图对于A=B+C的指令流水,先从B中取值放入寄存器R1中,在从C中取值放入R2中,然而,在ADD指令的计算阶段,由于时间与C的访存阶段重合,C还没有准备好,所以不得不中断一会儿,然后再进行计算。
下面有一个更复杂的例子:
a=b+c;
d=e-f;
![](https://i-blog.csdnimg.cn/blog_migrate/d63735e235838a1bdf7ab90248393c19.png)
可以看到会有很多中断。那么接下来看下指令重排序的魅力。
![](https://i-blog.csdnimg.cn/blog_migrate/7e07b26f213ae2a7c9c6316cd751625f.png)
由于后面的d=e-f对于a=b+c是没有影响的,那么可以将e、f加载到寄存器的指令提前进行,重排序后的结果:
![](https://i-blog.csdnimg.cn/blog_migrate/7a4aa57ca8f08a32251494aedf4d582f.png)
很明显,重排序后不需要插入中断了,这样是不是提高了运行效率咧。
内存系统重排序
由于处理器使用了缓存和读写缓冲区,常常打乱指令执行顺序。严格意义上来说,我个人认为这更像是一种内存模型带来的副作用
比如下面代码:
处理器A:
a=1;//A1
x=b;//A2
处理器B:
b=2;//B1
y=a;//B2
初始状态:
a=b=0;
![](https://i-blog.csdnimg.cn/blog_migrate/432d08ba6170f00f3a8ebf4ff80d2743.png)
上述代码是不是一定执行结果为x=2,y=1;呢,相信有并发编程经验的都知道不一定。这里表面上看,A1先执行,a=1写入到缓冲区中,此时执行A2读取内存的b=0,注意这时,a=1还没有刷新到内存中呢!这样在外界看来,处理器A就是先执行了A2再执行A1的!那么这个改变符合规定吗?答案是当然符合,对于处理器A来说,它把该做的都做了,也没有违反自己的串行语义。如果处理器B也是这样,最后是有可能出现x=y=0这样的结果的!
然而,个人认为,这个重排序并不带有“主观性”,只是缓存机制带来的副作用而已。
对于处理器重排序,可以使用内存屏障来禁止,比如volatile,关于volatile和synchronized后面会专门写文章学习。
happens-before
对于两个操作,有如下情况可以保证它们的顺序性。这里的两个操作可以在同一线程中,也可以在不同线程中。
- 程序次序规则:在一个线程中,程序代码中顺序,写在前面的代码次序先于后面
- 管程锁定规则:一个unlock操作先于后面对同一个锁的lock操作
- 线程启动规则:Thread对象的start()操作先于线程的所有操作
- 线程终止规则:线程的所有操作先于对此线程的终止检测
- 线程中断规则:interrupt操作先于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成先于该对象的finalize()方法
- volatile变量规则:volatile变量的写操作先于后面对这个变量的读操作
- 传递性:A先于B ,B先于C,则A先于C
总结
这篇博客讲了内存模型的架构,内存交互操作,JMM三大特性,指令重排序,happens-before规则等JMM内容,此后还会继续学习volatile、final、synchronize的相关内存语义和原理。