前言
要想深入了解 Java 并发编程, 就要先理解好 Java 内存模型, 而要理解 Java 内存模型又不得不从硬件, 计算机内存模型说起, 本文从计算机内存模型产生的原因, 解决的问题谈起, 然后再对 Java 模型进行介绍, 最后对计算机内存模型和 Java 内存模型进行总结, 希望大家看完本文之后有所收获!
CPU 工作过程及出现的问题
CPU 执行过程
大家都知道, 计算机在执行程序时, 每条指令都是在 CPU 中执行的, 而执行的时候, 又免不了要和数据打交道, 而计算机上面的临时数据, 是储存在主存中的.
计算机内存包括高速缓存和主存.
我们知道 CPU 执行指令的速度比从主存读取数据和向主存写入数据快很多, 所以为了高效利用 CPU,CPU 增加了 ** 高速缓存(cache)** 来匹配 CPU 的执行速度, 最终程序的执行过程如下
首先会将数据从主存中复制一份到 CPU 的高速缓存中
当 CPU 执行计算的时候就可以直接从高速缓存中读取数据和写入数据
当运算结束后, 再将高速缓存的数据更新到主存中
缓存一致性问题
上面的执行过程在单线程情况下并没有问题, 但是在多线程情况下就会出现问题, 因为 CPU 如果含有多个核心, 则每个核心都有自己独占高速缓存, 如果出现多个线程同时执行同一个操作, 那么结果是无法预知. 例如 2 个线程同时执行 i++, 假设 i 的初始值是 0, 那么我们希望 2 个线程执行完成之后 i 的值变为 2, 但是事实会是这样吗?
可能出现的情况有:
线程 1 先将 i=0 从主存中读取到线程 1 的高速缓存中, 然后 CPU 完成运算, 再将 i=1 写入到主存中, 然后线程 2 开始从主存中读取 i=1 到线程 2 的高速缓存中, 然后 CPU 完成运算, 再将 i=2 写入到主存中, 那么 i=2 即为我们想要的结果.
线程 1 将 i=0 从主存中读取到线程 1 的高速缓存中的同时线程 2 也从主存中读取 i=0 到线程 2 的高速缓存中, 然后线程 1 和线程 2 完成运算后, 也都将 i=1 写入到主存中, 那么结果 i=1, 结果就不是我们想要的了. 出现这个情况, 我们称为缓存不一致问题.
那么如何解决 CPU 出现的缓存不一致问题呢? 通常使用的解决方法有 2 种:
通过给总线加锁
使用缓存一致性协议
第 1 种方法虽然也达到了目的, 但是在总线被锁住的期间, 其他的 CPU 也无法访问主存, 效率很低, 所以就出现了缓存一致性协议即第 2 种方法, 其中最出名的就是 Intel 的 MESI 协议, MESI 协议保证每个 CPU 高速缓存中的变量都是一致的. 它的核心思想是, 当 CPU 写数据时候, 如果发现操作的变量是共享变量(即其他 CPU 上也存在该变量), 就会发出信号通知其他 CPU 将它高速缓存中缓存这个变量的缓存行置为无效状态, 因此当其他 CPU 需要读取这个变量时, 发现自己高速缓存中缓存该变量的缓存行为无效状态, 那么它就会从主存中重新读取.
处理器重排序问题
在多线程场景下, CPU 除了会出现缓存一致性问题, 还会出现因为处理器重排序即处理器 (CPU) 为了提高效率可能会对输入的代码进行乱序执行, 而造成多线程的情况下出现问题. 例如:
- // 线程 1:
- context = loadContext(); // 语句 1
- inited = true; // 语句 2
- // 线程 2:
- while(!inited ){
- sleep()
- }
- doSomethingwithconfig(context);
线程 1 由于处理器重排序, 先执行性了语句 2, 那么此时线程 2 会认为 context 已经初始化完成, 那么跳出循环, 去执行 doSomethingwithconfig(context)方法, 实际上此时 context 并未初始化(即线程 1 的语句 1 还未执行), 而导致程序出错.
什么是计算机内存模型
上面提到的缓存一致性问题, 处理器重排序问题都是在多线程情况下 CPU 可能出现的问题, 那我们应该怎么处理这些问题? 实际上这些问题并不需要我们考虑, 这些问题 CPU 都会处理好, 而 CPU 处理这些问题的时候是按照特定的操作规范, 对特定的主存进行访问或告诉 CPU 高速缓存怎么访问主存, 保证了多线程场景下的原子性, 可见性, 有序性, 这个操作规范就称为计算机内存模型.
可见性即当一个变量修改后, 这个变量会马上更新到主存中, 其他线程会收到通知这个变量修改过了, 使用这个变量的时候重新去主存获取
什么是 Java 内存模型
从前面的介绍了解到计算机内存模型是一种解决多线程场景下的一个主存操作规范, 既然是规范, 那么不同的编程语言都可以遵循这种操作规范, 在多线程场景下访问主存保证原子性, 可见性, 有序性.
Java 内存模型 (Java Memory Model,JMM) 即是 Java 语言对这个操作规范的遵循, JMM 规定了所有的变量都存储在主存中, 每个线程都有自己的工作区, 线程将使用到的变量从主存中复制一份到自己的工作区, 线程对变量的所有操作 (读取, 赋值等) 都必须在工作区, 不同的线程也无法直接访问对方工作区, 线程之间的消息传递都需要通过主存来完成. 可以把这里主存类比成计算机内存模型中的主存, 工作区类比成计算机内存模型中的高速缓存.
而我们知道 JMM 其实是工作主存中的, Java 内存模型中的工作区也是主存中的一部分, 所以可以这样说 Java 内存模型解决的是内存一致性问题 (主存和主存) 而计算机内存模型解决的是缓存一致性问题(CPU 高速缓存和主存), 这两个模型类似, 但是作用域不一样, Java 内存模型保证的是主存和主存之间的原子性, 可见性, 有序性, 而计算机内存模型保证的是 CPU 高速缓存和主存之间的原子性, 可见性, 有序性.
总结
本文很多观点都是按照笔者自己的理解然后总结出来的, 若有偏颇, 欢迎指正!
参考
Java 并发编程: volatile 关键字解析 https://www.cnblogs.com/dolphin0520/p/3920373.html
Java 内存模型
[教程] 终于有人把 Java 内存模型说清楚了! http://developer.51cto.com/art/201807/579744.htm
关于 JAVA 内存模型与 MESI 协议? https://www.zhihu.com/question/268021813
有了缓存一致性协议为什么还需要多线程同步? https://www.zhihu.com/question/277395220
来源: https://juejin.im/post/5c8610056fb9a049b222b366