| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> 面试必备系列JUC(2) -- java内存模型(JMM)超详解 -> 正文阅读 |
|
[Java知识库]面试必备系列JUC(2) -- java内存模型(JMM)超详解 |
《笑傲江湖》里面,写了太多的尔虞我诈,欺骗陷害,仿佛这个江湖有无穷的黑暗和污浊。可在这个背景里,却永远有一个人,是没有被这个世界的种种,染下一丝污渍的。这个人心里,一直是最纯澈,最透明的,她就是仪琳。在这个大男人们为了权力、为了欲望,而无所不用其及的江湖中,只有她,心心念念的只是一个令狐大哥。 (秋叶掉落在树上) 仪琳:令狐大哥,我最近在看JUC相关的知识,可是对JMM的内容一知半解,不知令狐大哥今天能否给小妹讲讲? 令狐冲:仪琳师妹这就见外了,在介绍Java内存模型之前,咱们先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情。 1.为什么要有计算机内存模型以及带来的问题仪琳:那为什么要有计算机内存模型呢? 令狐冲:所谓内存模型,是与计算机硬件有关的一个概念。那么我先给你介绍下他和硬件到底有啥关系。 1.1 现代计算机内存模型令狐冲:计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。 仪琳:可是,不能因为内存的读写速度慢,就不发展CPU技术了吧,总不能让内存成为计算机处理的瓶颈吧? 令狐冲:基于这种情况,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。 仪琳:这样的话,程序的执行过程就变成了:程序在运行时,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。 令狐冲:是的,但是随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。你看看下边这个图: 仪琳:这个图我知道,这是CPU的流行架构,当CPU要load一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。 ?1.2?缓存一致性问题仪琳:听说这里有一个面试常问缓存一致性问题,你能说说吗? 令狐冲:是的,我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。一定要仔细听哦! 在单线程中:cpu内核的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 在单核CPU、多线程环境下:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 在多核CPU、多线程环境下:每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个内核都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 仪琳:这就是著名的缓存一致性问题吧!之前就听说在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。 1.3 如何解决缓存一致性令狐冲:那你知道如何解决缓存一致性问题吗? 仪琳:为了解决缓存不一致性问题,通常来说有以下2种解决方法: ? ? ? ? ? ? 1)通过在总线加LOCK锁的方式 ? ? ? 2)通过缓存一致性协议 令狐冲:那为啥在总线上加LOCK锁可以解决呢? 仪琳:其实在早期的CPU当中,都是通过这种方式来做的。由于CPU和其他器件进行通信都是通过总线来进行的,如果对总线加LOCK锁的话,也就阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。只有当前的CPU完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。 令狐冲:但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。后来是怎么做的呢? 仪琳:后来就出现了缓存一致性协议,最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。 仪琳:核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 令狐冲:是的,在三级缓存架构中我们看到了,L3 cache和主存是共享的,所以就存在数据一致性的保障,MESI缓存一致性协议就作用在这里。MESI协议规定了CPU从主存(或者三级缓存)加载或者写入数据的规则,保证了数据的强一致性。 1.4 处理器优化和指令重排令狐冲:除了缓存一致性问题之外,还有一种硬件问题也比较重要,那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行优化排序,以方便执行处理,这就是处理器优化。 当然,除了处理器会对代码进行优化之外,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排。 可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。 ?2. 并发编程中经典三问题仪琳:前面说的和硬件有关的概念,我听得有点蒙,却依然不知道它到底和软件有啥关系? 令狐冲:其实前面说到的:缓存一致性问题、处理器优化乱序、指令重排问题,就是软件并发编程中的三个经典问题:原子性问题,可见性问题,有序性问题。 仪琳:这又是什么? 2.1 原子性令狐冲:先说下原子性,即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。(要么全部执行成功,要么全部失败) 仪琳:听不太懂,你能举个实际的例子吗? 令狐冲:举个最简单的例子,你想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
令狐冲: 假设一个线程执行到这条代码,我在这里假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。 仪琳:那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i变量的值,那么读取到的就是错误的数据。这就无法保证原子性了。 令狐冲:没错!记住这个例子。 2.2?可见性仪琳:可见性呢?是什么? 令狐冲:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。我举个简单的例子:
令狐冲:假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,但是却没有立即写入到主存当中。 令狐冲:此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10. 令狐冲:这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。 2.3 有序性仪琳:我懂了,那什么是有序性呢? 令狐冲:有序性:即程序执行的顺序按照代码的先后顺序执行。这个有点复杂,看下面这个例子吧:
仪琳:从代码顺序上看,语句1是在语句2前面的,那JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 令狐冲:不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。 仪琳:指令重排序我从书上看过,就是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 令狐冲:没错,比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。 仪琳:指令重排序会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? 令狐冲:我给你举个例子吧。
令狐冲:这段代码有4个语句,其中一个可能的执行顺序是: ????????语句2---》 语句1---》 语句3---》 语句4 仪琳:那可不能是下边这个执行顺序呢? ????????语句3---》 语句1---》 语句2---》 语句4 令狐冲:这个不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性。如果一个指令Ins?2必须用到Ins?1的结果,那么处理器会保证Ins 1会在Ins?2之前执行。 仪琳:那么说,重排序不会影响单个线程内程序执行的结果,但是多线程呢? 令狐冲:好问题,还是先看个例子吧。
令狐冲:上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomething(conf)方法,而此时conf并没有被初始化,就会导致程序出错。 仪琳:所以,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 令狐冲:是的,如果要想并发程序正确地执行,必须要保证原子性、可见性以及有序性,且缺一不可。 3. java内存模型令狐冲:前面介绍过了计算机内存模型,这是解决多线程场景下并发问题的一个重要规范。那么具体的实现是如何的呢,不同的编程语言,在实现上可能有所不同。我们在此处只聊java语言如何实现的。 令狐冲:Java内存模型(Java Memory Model)即JMM是一个抽象的概念,JMM是一个抽象的概念,并不是物理上的内存划分,一定要明白! 令狐冲:Java内存模型(JMM)定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作规范。在硬件内存模型中,各种CPU架构的实现是不尽相同的,Java作为跨平台的语言,为了屏蔽底层硬件差异,定义了Java内存模型(JMM)。JMM作用于JVM和底层硬件之间,屏蔽了下游不同硬件模型带来的差异,为上游开发者提供了统一的使用接口。 仪琳:其实你说了这么多其实就是想说明白JMM——JVM——硬件的关系。总之一句话,JMM是JVM的内存使用规范,是一个抽象的概念。 令狐冲:在JMM中,内存划分为两个区域,线程本地内存,主内存。 令狐冲:Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。 仪琳:是的,而JMM就作用于工作内存和主存之间数据同步过程。JMM规定了如何做数据同步以及什么时候做数据同步。 仪琳:那么Java语言本身对原子性、可见性以及有序性提供了哪些保证呢? 3.1 java如何实现原子性令狐冲:在Java中,为了保证原子性,提供了两个高级的字节码指令
|
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 9:08:07- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |