多线程&高并发深入浅出JMM-Java线程内存模型

Posted Roninaxious

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程&高并发深入浅出JMM-Java线程内存模型相关的知识,希望对你有一定的参考价值。

实际上JMM是虚拟机根据计算机内存模型模拟而来的,所以说理解计算机内存模型也是很重要的。

为什么Java要引入JMM这个概念?

由于Java是一门跨平台的语言,所以要屏蔽系统间的差异性;不同系统之间CPU和主存之间的交互速度是有差异的。所以JMM就应运而生了。【举例来说有可能Linux系统中CPU和主存速度比达到1000:1,Mac系统CPU和主存速度比达到1500:1;所以不可能设计多个规范,苦的还是我们】

在早期,CPU执行指令的速度和主存的存取速度是差不多的;经过不断地更新和迭代,CPU执行指令的速度已经远远超过了主存的存取速度;这时就诞生了一个问题,CPU执行读写指令之后还要在那等着主存,即浪费了CPU的运算单元。

🌀注意:高速缓存包含了CPU中的寄存器,以及三级缓存等。
内存包含了RAM和ROM
(随机存取存储器和只读存储器)

那么是如何解决CPU处理器和主存的速度矛盾的呢?

加入了一层读写速度尽可能接近处理器速度的高速缓存来作为处理器和主存之间的缓冲。当需要写入某个数据到主存时,可以直接写入到缓存,然后在运算结束后,将数据刷新到主存。当需要某个数据时直接从缓存中取即可。

如今大部分的计算机都引入了三级缓存机制,即L1, L2, L3

越往下,容量越大,也就意味着速度越来越慢。

引入了高速缓存会不会出现什么问题呢?

如今的计算机一般都是多核CPU,每个处理器都有自己的高速缓存,但它们又共享同一主存;所以当它们同时对某个变量进行修改时会出现缓存一致性问题。


当某个处理器1正在使用X=1这个变量,然后处理器2将这个X=1修改为了X=2;然后处理器1还在使用旧值。这就出现很大的问题了。

除了上述缓存一致性问题之外,还有一种问题。

为了能够充分利用CPU的运算单元,引入了指令重排这个概念。也就是CPU按照某种规则对执行的指令进行重新排序以达到最快的速度执行完毕。

看下面的两条指令

有可能CPU会对这两个指令进行重排,重排之后结果如下

为什么会这样?

为了最大可能的利用运算单元,提高性能。符合某种规范进行重排。

JMM内存模型


在多核处理器机器中,多个线程可能会同时执行;当需要intFlag变量时,会拷贝一份副本到自己的工作内存中。每个线程都有各自的工作内存,互不影响。

那么主存和工作内存是怎么交互的呢?

这涉及到了8个原子操作

lock :将变量标记为线程独有状态
read:将变量从主内存中读取出来
load:将read的值放到工作内存的变量副本中
use:将工作内存中的值传递给执行引擎
assign:将执行引擎操作结果赋值给工作内存中的变量
store:将工作内存中的变量传送到主内存中
write:将store的值赋值给主内存中的变量
unlock:解除变量的线程独有状态

Java内存模型与计算机内存模型之间的关系
可以把Java中的主存同RAM(随机存取存储器)对应起来,将工作内存类比Cache或寄存器。

当线程需要某个数据时,处理器首先去寄存器寻找,如果不存尝试读Cache,最后才是ROM

注意如今的Cache一般都是三级缓存,即L1,L2,L3

你会不会有这样的疑问:CPU第一次读取数据时怎么会命中寄存器或者Cache呢?

每个线程的工作内存会预先把需要的数据复制到Cache和寄存器中,但是不能保证所有的工作内存的变量副本都在Cache中,也有可能在RAM中,具体要看JVM的是如何实现的。

通过JMM演示多线程的可见性问题

public class Visibility 
    private static boolean  initFlag= true;
    public static void main(String[] args) throws InterruptedException 
        new Thread(() -> 
            while (initFlag) 

            
        ).start();
        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> 
            initFlag= false;
            System.out.println(Thread.currentThread().getName() + "线程将initFlag修改为了false!");
        ).start();
    


你会发现程序一直处于运行中,这是什么原因呢?

通过JMM进行分析

当线程2对initFlag做修改之后并同步到主内存中,但是线程1毫不知情。所以线程1就一直在那做while死循环。

如何解决线程间的可见性问题呢?


使用volatile关键字修饰即可。【volatitle能够保证线程间共享变量的可见性】
那么它的底层是怎么实现的呢?


主要是通过MESI缓存一致性协议来实现的;当线程2对initFlag修改为true之后,处理器2会立即将修改后的数据从缓存中同步到主内存,在这个同步的过程中会经过总线,会被处理器1的总线嗅探机制监听到initFlag发生了变化,处理器1会立即将缓存的initFlag=false失效。等到下次再使用initFlag时,需要重新从主内存读取。这样就保证了线程间共享变量的可见性。


对以上代码进行反汇编处理,查看底层volatile的实现原理

教你几步将Java代码生成汇编指令:https://blog.csdn.net/Kevinnsm/article/details/121695215?spm=1001.2014.3001.5502


Ctrl+F搜索lock关键字

可以看出第十五行代码在汇编指令中使用lock指令前缀

private static volatile boolean flag = true;

继续向下搜索lock出现的地方

可以看出第25行代码在底层汇编也使用了lock指令前缀

将flag修改为了false,使用lock前缀之后,CPU会将数据立即刷新到主存中,然后其他CPU根据总线嗅探机制,检测到flag值发生了变化,会立即将工作内存中的flag失效(因为同步到主内存需要经过总线)【多核计算机中,会有多个处理器,我这里说的CPU不是同一个】

以上是关于多线程&高并发深入浅出JMM-Java线程内存模型的主要内容,如果未能解决你的问题,请参考以下文章

多线程&高并发深入理解JMM产生的三大问题原子性可见性有序性

多线程&高并发深入浅出原子性

多线程&高并发深入浅出volatile关键字

Java多线程高并发学习笔记——深入理解线程池

认识多线程与高并发

Tomact高并发&Servlet线程处理