当前位置: 首页 > 科技观察

从硬件内存模型到Java内存模型,这些硬核知识你了解多少?

时间:2023-03-15 22:07:35 科技观察

Java的内存模型和上一篇文章中的JVM内存结构非常相似。我经常把它们混为一谈,但实际上它们不是一回事,有很大的区别。我希望你不要混淆他们,尤其是在采访中。当你一头雾水时,你会回答一些无关紧要的问题,影响你的面试表现。当然,你可能会遇到半吊子的面试官,恭喜你。Java内存模型比JVM内存结构复杂得多。Java内存模型有一个规范叫做:《JSR 133 :Java 内存模型与线程规范》,里面包含了很多内容。如果你还没有读过,我建议你看一看。今天我们就来简单聊聊Java内存模型。关于Java内存模型,我们先从硬件内存模型说起。硬件内存模型先来看一下硬件内存的简单架构,如下图所示:硬件内存结构这是一个简单的硬件内存结构图。真正的结构图要比这个复杂的多,尤其是在缓存层。现在的电脑国内一般有三层CPU缓存,大家也可以打开电脑看看,打开任务资源管理器--->性能--->cpu,如下图:CPU缓存可以从中看到图中我的机器CPU有三级缓存,一级缓存(L1),二级缓存(L2),三级缓存(L3)。一级缓存离CPU最近,三级缓存离内存最近。每一级缓存的所有数据都是下一级缓存的一部分。三级缓存架构如下图所示:图片来源网络既然我们对硬件内存架构有了一定的了解,那么我们来想一个问题,为什么要在CPU和内存之间加一个缓存呢?简单说一下这个问题,我们知道CPU是高速的,而内存是比较低速的,这会造成CPU的高速特性不能被充分发挥的问题,因为CPU需要每次从内存中获取数据都要等待,浪费为了保证CPU的高速性能,缓存的出现就是为了消除CPU和内存之间的空隙。缓存的速度大于内存,小于CPU。添加缓存后,CPU直接从缓存中读取数据,因为缓存还是比较快的,所以这样就充分利用了CPU的高速特性。但是不可能每次都从缓存中读取数据。这个和我们项目中使用的redis等缓存工具是一样的。还有缓存命中率。在CPU中,首先寻找L1Cache。如果L1Cache没有命中,就去L2Cache继续查找,以此类推。如果没有找到,就直接从内存中取出,然后加入到缓存中。当然,当CPU需要向主存写入数据时,也会先将寄存器中的数据刷新到CPU缓存中,然后再将数据刷新到主存中。也许你已经看到了这个框架的缺点。在单核时代,处理器核只有一个,读写操作全部由单核完成。如果不知道数据已经过期,继续傻傻地使用主存或者自己缓存层中的数据,就会导致数据不一致。CPU硬件厂商也针对这个问题提供了一种解决方案,称为缓存一致性协议(MESI协议)。我对缓存一致性协议一窍不通,也解释不清楚,BB这里就不贴了。如果你有兴趣,你可以自己研究。说完硬件内存架构,我们再回到我们的话题,Java内存模型。下面一起来聊聊Java内存模型。Java内存模型什么是Java内存模型?Java内存模型可以理解为遵循多核硬件架构的设计,使用Java在JVM层面实现了一套“缓存一致性”,从而避免了CPU硬件厂商标准不同带来的问题.的风险。好吧,下面正式介绍一下Java内存模型:Java内存模型(JavaMemoryModel,简称JMM),本身就是一个抽象的概念,不像硬件架构那么真实,它描述了一组规则或者规范,通过这个集合ofspecifications定义了程序中各种变量(包括实例字段、静态字段和构成数组对象的元素)的访问方法。有关Java内存模型的更多信息,您可以阅读JSR133:Java内存模型和线程规范。我们知道JVM运行程序的实体是线程。在前面的JVM内存结构中,我们了解到在创建每个线程时,JVM都会为其创建一个工作内存(Java栈)用于存放线程私有数据,而Java内存模型规定所有的变量都存放在main中记忆。主存是一个共享内存区,所有线程都可以访问。但是,线程对变量的操作(读赋值等)必须在工作内存中进行。首先,变量必须从主存中拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主存,不能直接操作主存中的变量.我们知道Java栈是每个线程的私有数据区。其他线程无法访问不同线程的私有数据。因此,如果线程之间需要通信,就必须通过主存来完成。Java内存模型夹在两者之间先来看看这张抽象架构图:从结构图来看,如果线程A和线程B需要通信,必须经过以下两步:首先,线程A将本地内存A中的共享变量副本中的值刷新到主存中。然后,线程B去主存中读取线程A更新后的值,从而使线程A中的变量值到达线程B中。下面看一个具体的例子来加深理解。看下图:图片来源网络现在线程A需要和线程B进行通信,线程间通信的两个步骤我们已经知道了。假设一开始,这三个内存中的x值都为0,线程A在执行时,将更新后的x值(假设为1)暂存在自己的本地内存A中,当线程A和线程B需要通信时,线程A会先将自己本地内存中修改后的x值刷新到主存中,此时主存中的x值变为1。随后,线程B去主存中读取线程A更新后的x值,此时线程B的本地内存中的x值也变为1,从而完成一次通信。JMM通过控制主内存和每个线程的本地内存之间的交互,为Java程序员提供内存可见性保证。Java内存模型除了定义了一套规范外,还提供了一系列原语,将底层实现封装起来,供开发者直接使用。这套实现就是我们常用的volatile、synchronized、final等。Happens-Before内存模型Happens-Before内存模型可以称为Happens-Before原理。在《JSR 133 :Java 内存模型与线程规范》中,Happens-Before内存模型被定义为Java内存模型的近似模型。Happens-Before原则是关于可见的一组偏序关系。为了方便程序员的开发,屏蔽底层繁琐的细节,Java内存模型定义了Happens-Before原则。只要理解Happens-Before原理,我们就可以在不知道JVM底层内存操作的情况下解决并发编程中遇到的变量可见性问题。JVM定义的Happens-Before原则是一组偏序关系:对于两个操作A和B,这两个操作可以在不同的线程中执行。如果AHappens-BeforeB,那么可以保证A操作执行后,A操作的执行结果对B操作可见。Happens-Before原则包括总共8条规则。让我们一起来简单地研究一下这8条规则。1.程序顺序规则这个规则是指在一个线程中,根据程序顺序,前面的操作happens-before任何后续操作。这个规则还是很容易理解的。看下面这段代码classTest{1intx;2inty;3publicvoidrun(){4y=20;5x=12;}}第四行代码应该发生在第五行代码之前,也就是按照的顺序代码来了。2.加锁规则这条规则是指一个锁的解锁Happens-Before随后的锁的加锁。比如下面的代码在进入同步块之前会自动加锁,代码块执行完后会自动释放锁。加锁和释放都是编译器帮我们实现的。synchronized(this){//这里自动加锁//x是一个共享变量,初始值=10if(this.x<12){this.x=12;}}//这里的自动解锁可以理解为加锁规则:假设x的初始值为10,线程A执行完代码块后,x的值会变成12(执行完自动释放锁)。当线程B进入代码块时,可以看到线程A对x的写操作,即线程B可以看到x==12.3。volatile变量的规则这个规则是指对一个volatile变量的写操作和这个写操作之前的所有操作Happens-Before对这个变量的读操作和这个读操作之后的所有操作。4.线程启动规则这条规则是指主线程A启动子线程B后,子线程B在启动子线程B之前可以看到主线程的运行情况。publicclassDemo{privatestaticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{System.out.println(count);});count=12;t1.start();}}子线程t1可以看到主线程对count变量的修改,所以在线程中打印了12。这是线程启动规则5、线程结束规则这是关于线程等待的。意思是主线程A等待子线程B完成(主线程A是通过调用子线程B的join()方法实现的),当子线程B完成时(join()方法在主线程A返回),主线程可以看到子线程的运行情况。当然,所谓“看见”是指对共享变量的操作。publicclassDemo{privatestaticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{//t1线程修改变量count=12;});t1.start();t1.join();//mian线程可以看到t1线程修改的变量System.out.println(count);}}6.中断规则一个线程在另一个线程上调用中断,Happens-在被中断的线程检测到中断被调用之前。publicclassDemo{privatestaticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{//t1线程可以看到被中断前的数据System.out.println(count);});t1.start();count=25;//t1线程被中断t1.interrupt();}}mian线程中调用了t1线程的interrupt()方法,mian对count的修改对t1线程可见。7.Finalizer规则一个对象的构造函数执行结束Happens-Before其finalize()方法开始之前。“结束”和“开始”表示在时间上,对象的构造函数必须在其finalize()方法被调用时执行。根据这个原则,可以保证在执行对象的finalize方法时,对象的所有field字段值都是可见的。8.传递规则这个规则意味着如果AHappens-BeforeB,并且BHappens-BeforeC,那么AHappens-BeforeC。