文章收拾自 博学谷狂野架构师

敞开成长之旅!这是我参加「日新计划 2 月更文应战」的第 9 天,点击检查活动概况

什么是JMM

来一波骚操作,Java内存模型

并发编程范畴的关键问题

线程之间的通讯

线程的通讯是指线程之间以何种机制来交换信息。在编程中,线程之间的通讯机制有两种,同享内存和音讯传递。 ​ 在同享内存的并发模型里,线程之间同享程序的公共状况,线程之间经过写-读内存中的公共状况来隐式进行通讯,典型的同享内存通讯方式便是经过同享目标进行通讯。 在音讯传递的并发模型里,线程之间没有公共状况,线程之间有必要经过清晰的发送音讯来显式进行通讯,在java中典型的音讯传递方式便是wait()和notify()。

线程间的同步

同步是指程序用于操控不同线程之间操作发生相对次序的机制。

在同享内存并发模型里,同步是显式进行的。程序员有必要显式指定某个办法或某段代码需求在线程之间互斥履行。 ​ 在音讯传递的并发模型里,由于音讯的发送有必要在音讯的接纳之前,因而同步是隐式进行的。

现代核算机的内存模型

物理核算机中的并发问题,物理机遇到的并发问题与虚拟机中的状况有不少类似之处,物理机对并发的处理计划关于虚拟机的完结也有相当大的参阅含义。

其间一个重要的复杂性来源是绝大多数的运算使命都不或许只靠处理器“核算”就能完结,处理器至少要与内存交互,如读取运算数据、存储运算成果等,这个I/O操作是很难消除的(无法仅靠寄存器来完结一切运算使命)。

前期核算机中cpu和内存的速度是差不多的,但在现代核算机中,cpu的指令速度远超内存的存取速度,由于核算机的存储设备与处理器的运算速度有几个数量级的距离,所以现代核算机体系都不得不加入一层读写速度尽或许挨近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需求运用到的数据复制到缓存中,让运算能快速进行,当运算完毕后再从缓存同步回内存之中,这样处理器就无须等候缓慢的内存读写了。

依据高速缓存的存储交互很好地处理了处理器与内存的速度对立,但是也为核算机体系带来更高的复杂度,由于它引入了一个新的问题:缓存共同性(Cache Coherence)。

在多处理器体系中,每个处理器都有自己的高速缓存,而它们又同享同一主内存(MainMemory)。当多个处理器的运算使命都触及同一块主内存区域时,将或许导致各自的缓存数据不共同,举例阐明变量在多个CPU之间的同享。

假设真的发生这种状况,那同步回到主内存时以谁的缓存数据为准呢?为了处理共同性的问题,需求各个处理器拜访缓存时都遵从一些协议,在读写时要依据协议来进行操作,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。

来一波骚操作,Java内存模型

该内存模型带来的问题

现代的处理器运用写缓冲区临时保存向内存写入的数据。写缓冲区能够确保指令流水线持续运转,它能够避免由于处理器停顿下来等候向内存写入数据而发生的推迟。

一起,经过以批处理的方式改写写缓冲区,以及兼并写缓冲区中对同一内存地址的多次写,削减对内存总线的占用。

虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的履行次序发生重要的影响:处理器对内存的读/写操作的履行次序,不一定与内存实践发生的读/写操作次序共同! ​ 处理器A和处理器B按程序的次序并行履行内存拜访,终究或许得到x=y=0的成果。 处理器A和处理器B能够一起把同享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个同享变量(A2,B2),终究才把自己写缓存区中保存的脏数据改写到内存中(A3,B3)。

当以这种时序履行时,程序就能够得到x=y=0的成果。 ​ 从内存操作实践发生的次序来看,直到处理器A履行A3来改写自己的写缓存区,写操作A1才算实在履行了。虽然处理器A履行内存操作的次序为:A1→A2,但内存操作实践发生的次序却是A2→A1。

Processor A Processor B
代码 a=1; //A1 x=1; //A2 b=2; //B1 y=a; //B2
运转成果 初始状况 a=b=0 处理器答应得到成果 x=y=0

来一波骚操作,Java内存模型

Java内存模型界说

JMM界说了Java 虚拟机(JVM)在核算机内存(RAM)中的作业方式。JVM是整个核算机虚拟模型,所以JMM是隶归于JVM的。

从笼统的角度来看,JMM界说了线程和主内存之间的笼统联系:线程之间的同享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写同享变量的副本。

本地内存是JMM的一个笼统概念,并不实在存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

来一波骚操作,Java内存模型

Java内存区域

来一波骚操作,Java内存模型

Java虚拟机在运转程序时会把其自动办理的内存区分为以上几个区域,每个区域都有的用处以及创立销毁的时机,其间蓝色部分代表的是一切线程同享的数据区域,而紫色部分代表的是每个线程的私有数据区域。

办法区

办法区归于线程同享的内存区域,又称Non-Heap(非堆),首要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,依据Java 虚拟机标准的规矩,当办法区无法满意内存分配需求时,将抛出OutOfMemoryError 反常。

值得留意的是在办法区中存在一个叫运转时常量池(Runtime Constant Pool)的区域,它首要用于存放编译器生成的各种字面量和符号引证,这些内容将在类加载后存放到运转时常量池中,以便后续运用。

JVM堆

Java 堆也是归于线程同享的内存区域,它在虚拟机启动时创立,是Java 虚拟机所办理的内存中最大的一块,首要用于存放目标实例,简直一切的目标实例都在这儿分配内存,留意Java 堆是垃圾收集器办理的首要区域,因而许多时分也被称做GC 堆,假设在堆中没有内存完结实例分配,而且堆也无法再扩展时,将会抛出OutOfMemoryError 反常。

程序计数器

归于线程私有的数据区域,是一小块内存空间,首要代表当时线程所履行的字节码行号指示器。字节码解说器作业时,经过改变这个计数器的值来选取下一条需求履行的字节码指令,分支、循环、跳转、反常处理、线程康复等基础功用都需求依赖这个计数器来完结。

虚拟机栈

归于线程私有的数据区域,与线程一起创立,总数与线程关联,代表Java办法履行的内存模型。每个办法履行时都会创立一个栈桢来存储办法的的变量表、操作数栈、动态链接办法、回来值、回来地址等信息。每个办法从调用直完毕就关于一个栈桢在虚拟机栈中的入栈和出栈进程,如下(图有误,应该为栈桢):

本地办法栈

本地办法栈归于线程私有的数据区域,这部分首要与虚拟机用到的 Native 办法相关,一般状况下,咱们无需关怀此区域。

小结

这儿之所以扼要阐明这部分内容,留意是为了差异Java内存模型与Java内存区域的区分,究竟这两种区分是归于不同层次的概念。

Java内存模型概述

Java内存模型(即Java Memory Model,简称JMM)自身是一种笼统的概念,并不实在存在,它描绘的是一组规矩或标准,经过这组标准界说了程序中各个变量(包括实例字段,静态字段和构成数组目标的元素)的拜访方式。

由于JVM运转程序的实体是线程,而每个线程创立时JVM都会为其创立一个作业内存(有些当地称为栈空间),用于存储线程私有的数据,而Java内存模型中规矩一切变量都存储在主内存,主内存是同享内存区域,

一切线程都能够拜访,但线程对变量的操作(读取赋值等)有必要在作业内存中进行,首要要将变量从主内存复制的自己的作业内存空间,然后对变量进行操作,操作完结后再将变量写回主内存,不能直接操作主内存中的变量,作业内存中存储着主内存中的变量副本复制,

前面说过,作业内存是每个线程的私有数据区域,因而不同的线程间无法拜访对方的作业内存,线程间的通讯(传值)有必要经过主内存来完结,其扼要拜访进程如下图

来一波骚操作,Java内存模型

需求留意的是,JMM与Java内存区域的区分是不同的概念层次,更恰当说JMM描绘的是一组规矩,经过这组规矩操控程序中各个变量在同享数据区域和私有数据区域的拜访方式,JMM是围绕原子性,有序性、可见性打开的(稍后会分析)。

JMM与Java内存区域仅有类似点,都存在同享数据区域和私有数据区域,在JMM中主内存归于同享数据区域,从某个程度上讲应该包括了堆和办法区,而作业内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地办法栈。

或许在某些当地,咱们或许会看见主内存被描绘为堆内存,作业内存被称为线程栈,实践上他们表达的都是同一个含义。关于JMM中的主内存和作业内存阐明如下

主内存

首要存储的是Java实例目标,一切线程创立的实例目标都存放在主内存中,不管该实例目标是成员变量还是办法中的本地变量(也称局部变量),当然也包括了同享的类信息、常量、静态变量。

由于是同享数据区域,多条线程对同一个变量进行拜访或许会发现线程安全问题。

作业内存

首要存储当时办法的一切本地变量信息(作业内存中存储着主内存中的变量副本复制),每个线程只能拜访自己的作业内存,即线程中的本地变量对其它线程是不行见的,就算是两个线程履行的是同一段代码,它们也会各自在自己的作业内存中创立归于当时线程的本地变量,当然也包括了字节码行号指示器、相关Native办法的信息。

留意由于作业内存是每个线程的私有数据,线程间无法彼此拜访作业内存,因而存储在作业内存的数据不存在线程安全问题。

数据同步

弄清楚主内存和作业内存后,接了解一下主内存与作业内存的数据存储类型以及操作方式,依据虚拟机标准,关于一个实例目标中的成员办法而言,假设办法中包括本地变量是根本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在作业内存的帧栈结构中,但假使本地变量是引证类型,那么该变量的引证会存储在功用内存的帧栈中,而目标实例将存储在主内存(同享数据区域,堆)中。

但关于实例目标的成员变量,不管它是根本数据类型或许包装类型(Integer、Double等)还是引证类型,都会被存储到堆区。

至于static变量以及类自身相关信息将会存储在主内存中。需求留意的是,在主内存中的实例目标能够被多线程同享,假使两个线程一起调用了同一个目标的同一个办法,那么两条线程会将要操作的数据复制一份到自己的作业内存中,履行完结操作后才改写到主内存,简略示意图如下所示:

来一波骚操作,Java内存模型

硬件内存架构与Java内存模型

硬件内存架构

来一波骚操作,Java内存模型

正如上图所示,经过简化CPU与内存操作的简易图,实践上没有这么简略,这儿为了了解方便,咱们省去了南北桥并将三级缓存统一为CPU缓存(有些CPU只要二级缓存,有些CPU有三级缓存)。

就现在核算机而言,一般具有多个CPU而且每个CPU或许存在多个中心,多核是指在一枚处理器(CPU)中集成两个或多个完好的核算引擎(内核),这样就能够支撑多使命并行履行,从多线程的调度来说,每个线程都会映射到各个CPU中心中并行运转。

在CPU内部有一组CPU寄存器,寄存器是cpu直接拜访和处理的数据,是一个临时放数据的空间。一般CPU都会从内存取数据到寄存器,然后进行处理,但由于内存的处理速度远远低于CPU,导致CPU在处理指令时往往花费许多时刻在等候内存做准备作业

于是在寄存器和主内存间添加了CPU缓存,CPU缓存比较小,但拜访速度比主内存快得多,假设CPU总是操作主内存中的同一址地的数据,很容易影响CPU履行速度,此时CPU缓存就能够把从内存提取的数据暂时保存起来,假设寄存器要取内存中同一位置的数据,直接从缓存中提取,无需直接从主内存取。

需求留意的是,寄存器并不每次数据都能够从缓存中获得数据,假设不是同一个内存地址中的数据,那寄存器还有必要直接绕过缓存从内存中取数据。

所以并不每次都得到缓存中取数据,这种现象有个专业的称号叫做缓存的射中率,从缓存中取就射中,不从缓存中取从内存中取,就没射中,可见缓存射中率的凹凸也会影响CPU履行性能,这便是CPU、缓存以及主内存间的扼要交互进程,

总而言之当一个CPU需求拜访主存时,会先读取一部分主存数据到CPU缓存(当然假设CPU缓存中存在需求的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需求写数据到主存时,同样会先改写寄存器中的数据到CPU缓存,然后再把数据改写到主内存中。

Java线程与硬件处理器

了解完硬件的内存架构后,接着了解JVM中线程的完结原理,了解线程的完结原理,有助于咱们了解Java内存模型与硬件内存架构的联系,在Window体系和Linux体系上,Java线程的完结是依据1对1的线程模型,所谓的1对1模型,实践上便是经过言语级别层面程序去直接调用体系内核的线程模型,即咱们在运用Java线程时,Java虚拟机内部是转而调用当时操作体系的内核线程来完结当时使命。

这儿需求了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作体系内核(Kernel)支撑的线程,这种线程是由操作体系内核来完结线程切换,内核经过操作调度器进而对线程履行调度,并将线程的使命映射到各个处理器上。每个内核线程能够视为内核的一个分身,这也便是操作体系能够一起处理多使命的原因。

由于咱们编写的多线程程序归于言语层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是一般含义上的线程,由于每个轻量级进程都会映射到一个内核线程,因而咱们能够经过轻量级进程调用内核线程,进而由操作体系内核将使命映射到各个处理器,这种轻量级进程与内核线程间1对1的联系就称为1对1的线程模型。如下图

来一波骚操作,Java内存模型

如图所示,每个线程终究都会映射到CPU中进行处理,假设CPU存在多核,那么一个CPU将能够并行履行多个线程使命。

Java内存模型与硬件内存架构的联系

经过对前面的硬件内存架构、Java内存模型以及Java多线程的完结原理的了解,咱们应该现已意识到,多线程的履行终究都会映射到硬件处理器上进行履行,但Java内存模型和硬件内存架构并不完全共同。

关于硬件内存来说只要寄存器、缓存内存、主内存的概念,并没有作业内存(线程私有数据区域)和主内存(堆内存)之分,也便是说Java内存模型对内存的区分对硬件内存并没有任何影响,由于JMM只是一种笼统的概念,是一组规矩,并不实践存在,

不管是作业内存的数据还是主内存的数据,关于核算机硬件来说都会存储在核算机主内存中,当然也有或许存储到CPU缓存或许寄存器中,因而总体上来说,Java内存模型和核算机硬件内存架构是一个彼此穿插的联系,是一种笼统概念区分与实在物理硬件的穿插。(留意关于Java内存区域区分也是同样的道理)

来一波骚操作,Java内存模型

当目标和变量能够存储在核算机的各种不同存储区域中时,或许会出现某些问题。两个首要问题是:

  • 线程更新(写入)同享变量的可见性。
  • 读取,检查和写入同享变量时的竞赛条件。
同享目标的可见性

假设两个或多个线程同享一个目标,而没有正确运用 volatile 声明或同步,则一个线程对同享目标的更改关于在其他CPU上运转的线程是不行见的。

这样,每个线程终究都或许具有自己的同享目标副本,每个副本都坐落不同的CPU缓存中,而且其间的内容不相同。

下图简略阐明了状况。在左边CPU上运转的一个线程将同享目标复制到其CPU缓存中,并将其 count 变量更改为2。此更改关于在CPU上运转的其他线程不行见,由于count的更新没有改写回主内存。

来一波骚操作,Java内存模型

要处理此问题,您能够运用Java的volatile关键字。volatile 关键字能够确保变量从主内存中直接读取而不是从缓存中,而且更新的时分总是当即写回主内存。

竞赛条件

假设两个或多个线程同享一个目标,而且多个线程更新该同享目标中的成员变量,则或许会出现竞赛条件。

幻想一下,假设线程A将同享目标的变量count读入其CPU缓存中。再幻想一下,线程B也做了同样的事情,但是进入到了不同的CPU缓存。现在线程A添加一个值到count,线程B履行相同的操作。现在var1现已添加了两次,每次CPU缓存一次。

假设这些添加操作按次序履行,则变量count将添加两次并将”原始值+ 2”后发生的新值写回主存储器。

但是,两个添加操作一起履行却没有进行恰当的同步。不管线程A和B中的哪一个将其更新版别count写回主到存储器,更新的值将仅比原始值多1,虽然有两个添加操作。

该图阐明了如上所述的竞赛条件问题的发生:

来一波骚操作,Java内存模型

要处理此问题,您能够运用Java synchronized块。同步块确保在任何给定时刻只要一个线程能够进入代码的临界区。同步块还确保在同步块内拜访的一切变量都将从主存储器中读入,当线程退出同步块时,一切更新的变量将再次改写回主存储器,不管变量是否声明为volatile。

JMM存在的必要性

在明白了Java内存区域区分、硬件内存架构、Java多线程的完结原理与Java内存模型的具体联系后,接着来谈谈Java内存模型存在的必要性。

由于JVM运转程序的实体是线程,而每个线程创立时JVM都会为其创立一个作业内存(有些当地称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作有必要经过作业内存直接完结,首要进程是将变量从主内存复制的每个线程各自的作业内存空间,然后对变量进行操作,操作完结后再将变量写回主内存,假设存在两个线程一起对一个主内存中的实例目标的变量进行操作就有或许诱发线程安全问题。

如下图,主内存中存在一个同享变量x,现在有A和B两条线程分别对该变量x=1进行操作,A/B线程各自的作业内存中存在同享变量副本x。

假定现在A线程想要修改x的值为2,而B线程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?答案是,不确定,即B线程有或许读取到A线程更新前的值1,也有或许读取到A线程更新后的值2,这是由于作业内存是每个线程私有的数据区域,而线程A变量x时,

首要是将变量从主内存复制到A线程的作业内存中,然后对变量进行操作,操作完结后再将变量x写回主内,而关于B线程的也是类似的,这样就有或许形成主内存与作业内存间数据存在共同性问题,假设A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1复制到自己的作业内存中,

这样B线程读取到的值便是x=1,但假设A线程已将x=2写回主内存后,B线程才开端读取的话,那么此时B线程读取到的便是x=2,但到底是哪种状况先发生呢?这是不确定的,这也便是所谓的线程安全问题。

来一波骚操作,Java内存模型

为了处理类似上述的问题,JVM界说了一组规矩,经过这组规矩来决定一个线程对同享变量的写入何时对另一个线程可见,这组规矩也称为Java内存模型(即JMM),JMM是围绕着程序履行的原子性、有序性、可见性打开的,下面咱们看看这三个特性。

本文由传智教育博学谷狂野架构师教研团队发布。

假设本文对您有协助,欢迎关注点赞;假设您有任何建议也可留言谈论私信,您的支撑是我坚持创作的动力。

转载请注明出处!