说点JMM 让你的面试锦上添花

[复制链接]
并发编程关键题目

JDK天生就是多线程的,多线程大大提速了法式运转的速度,可是凡事有益就有弊,并发编程时经常会触及到线程之间的通讯跟同步题目,一般也说是可见性、原子性、有序性。
线程通讯


线程的通讯是指线程之间经过什么机制来交换信息,在编程中常用的通讯机制有两个,同享内存消息传递
    同享内存。

在同享内存的并发模子中线程之间同享法式的公共数据状态,线程之前经过读写内存中的公共内存地区来停止信息的传递,典型的同享内存通讯方式就是经过同享工具来停止通讯。
    消息传递,比如在Linux系统中同步机制有管道、信号、消息行列、信号量、套接字这几种方式。

在消息传递的并发模子中,线程之间是没有同享状态的,线程之间必须经过明白的发送消息来显式的停止通讯,在Java中的典型通讯方式就是wait()跟notify()。

在C/C++中可以同时支持同享内存跟消息传递机制,Java中采用的是同享内存模子。



线程同步


同步是指法式用于控制分歧线程之间操纵发生相对顺序的机制。

在同享内存并发模子里,同步是显式停止的。法式员必须显式指定某个方式或某段代码需要在线程之间互斥履行。

在消息传递的并发模子里,由于消息的发送必须在消息的接收之前,是以同步是隐式停止的。
JMM

现代计较机物理上的内存模子

缓存


领会JMM前我们先领会下现代计较机物理上的数据存储模子。

说点JMM 让你的口试如虎添翼-1.jpg

随着CPU技术的成长,CPU的履行速度越来越快。而由于内存的技术并没有太大的变化,我履行一个使命一共耗时10秒,成果CPU获得数据耗时8秒,CPU计较耗时2秒,大部分时候都用来获得数据上了。

怎样处理这个题目呢?就是在CPU和内存之间增加高速缓存。缓存的概念就是保存一份数据拷贝。他的特点是速度快,内存小,而且高贵。

今后法式运转获得数据就是以下的步调了。

说点JMM 让你的口试如虎添翼-2.jpg

而且随着CPU计较才能的不竭提升,一层缓存就渐渐的没法满足要求了,就逐步的衍生出多级缓存。依照数据读取顺序和与CPU连系的慎密水平,CPU缓存可以分为一级缓存(L1),二级缓存(L2),部分高端CPU还具有三级缓存(L3),每一级缓存中所贮存的全数数据都是下一级缓存的一部分。这三种缓存的技术难度和制造本钱是相对递加的,所以其容量也是相对递增的,性能对照以下:

说点JMM 让你的口试如虎添翼-3.jpg

单核CPU只含有一套L1,L2,L3缓存;假如CPU含有多个焦点,即多核CPU,则每个焦点都含有一套L1(甚至和L2)缓存,而同享L3(大概和L2)缓存。

说点JMM 让你的口试如虎添翼-4.jpg

缓存分歧性


随着计较性才能不竭提升,起头支持多线程。那末题目就来了。我们别离来分析下单线程、多线程在单核CPU、多核CPU中的影响。
    单线程。cpu焦点的缓存只被一个线程拜候。缓存独占,不会出现拜候抵触等题目。单核CPU多线程。进程中的多个线程会同时拜候进程中的同享数据,CPU将某块内存加载到缓存后,分歧线程在拜候不异的物理地址的时辰,城市映照到不异的缓存位置,这样即使发生线程的切换,缓存照旧不会生效。但由于任何时辰只能有一个线程在履行,是以不会出现缓存拜候抵触。多核CPU,多线程。每个核都最少有一个L1 缓存。多个线程拜候进程中的某个同享内存,且这多个线程别离在分歧的焦点上履行,则每个焦点城市在各自的caehe中保存一份同享内存的缓冲。由于多核是可以并行的,能够会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有能够分歧。

在CPU和主存之间增加缓存,在多线程场景下便能够存在缓存分歧性题目,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容能够纷歧致。

说点JMM 让你的口试如虎添翼-5.jpg

缓存分歧性(Cache Coherence):在多处置器系统中,每个处置器都有自己的高速缓存,而它们又同享同一主内存(MainMemory)。当多个处置器的运算使命都触及同一块主内存地区时,将能够致使各自的缓存数据纷歧致,比如同享内存的一个变量在多个CPU之间的同享。假如真的发生这类情况,那同步回到主内存时以谁的缓存数据为准呢?为领会决分歧性的题目,需要各个处置器拜候缓存时都遵守一些协议,在读写时要按照协议来停止操纵,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。

纷歧致demo以下:
//线程A 履行以下a = 1 // A1x = b // A2-----// 线程B 履行以下b = 2 // B1y = a // B2复制代码
说点JMM 让你的口试如虎添翼-6.jpg

处置器A和处置器B按法式的顺序并行履行内存拜候,终极能够获得x=y=0的成果。

处置器A和处置器B可以同时把同享变量写入自己的写缓冲区(A1,B1),a=1,b=2。

写操纵a = 1,b = 2要经过A3跟B3 革新到同享缓存才算终了。

假如这一步在第二步履行前履行了(A2,B2),x=b,y=a。法式便可以获得x=y=0的成果。
处置器优化和指令重排


上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在缓存分歧性题目。除了这类情况,还有一种硬件题目也比力重要。那就是为了使处置器内部的运算单元可以只管的被充实操纵,处置器能够会对输入代码停止乱序履行处置。这就是处置器优化。

除了现在很多风行的处置器会对代码停止优化乱序处置,很多编程说话的编译器也会有类似的优化,比如Java的JIT

不可思议,假如任由处置器优化和编译器对指令重排的话,便能够致使各类百般的题目。硬件级别跟编译器级别城市对这些题目停止处理。
并发编程题目


前面说的都是跟硬件相关的题目,我们需要晓得软件的下层是硬件,软件在这样的层面上运转就会出现原子性、可见性、有序性题目。 实在,原子性题目,可见性题目和有序性题目。是人们笼统界说出来的。而这个笼统的底层题目就是前面提到的缓存分歧性、处置器优化、指令重排题目。

一般而言并发编程,为了保证数据的平安,需要满足以下三个特征:

原子性:指在一个操纵中就是cpu不成以在中途停息然后再调剂,既不被中断操纵,要不履行完成,要不就不履行。

可见性:指当多个线程拜候同一个变量时,一个线程点窜了这个变量的值,其他线程可以立即看获得点窜的值。

有序性:法式履行的顺序依照代码的前后顺序履行。

你可以发现缓存分歧性题目实在就是可见性题目。而处置器优化是可以致使原子性题目标。指令重排即会致使有序性题目。
内存模子


前面提到的,缓存分歧性、处置器优化、指令重排题目是硬件的不竭升级致使的。那末,有没有什么机制可以很好的处理上面的这些题目呢 为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是内存模子。

为了保证同享内存的正确性(可见性、有序性、原子性),内存模子界说了同享内存系统中多线程法式读写操纵行为的标准。经过这些法则来标准对内存的读写操纵,从而保证指令履行的正确性。它与处置器有关、与缓存有关、与并发有关、与编译器也有关。他处理了CPU多级缓存、处置器优化、指令重排等致使的内存拜候题目,保证了并发场景下的分歧性、原子性和有序性。

内存模子处理并发题目首要采用两种方式:限制处置器优化和利用内存屏障。
JMM


前面说到计较机内存模子是处理多线程场景下并发题目标一个重要标准。那末具体的实现是若何的呢,分歧的编程说话,在实现上能够有所分歧。

我们晓得,Java法式是需要运转在Java虚拟机上面的,Java内存模子(Java Memory Model ,JMM)就是一种合适内存模子标准的,屏障了各类硬件和操纵系统的拜候差别的,保证了Java法式在各类平台下对内存的拜候都能保证结果分歧的机制及标准。

提到Java内存模子,一般指的是JDK 5 起头利用的新的内存模子,首要由JSR-133: JavaTM Memory Model and Thread Specification 描写。简单形象图以下:

说点JMM 让你的口试如虎添翼-7.jpg

JMM功用:

这是一种虚拟的标准,感化于工作内存和主存之间数据同步进程。目标是处理由于多线程经过同享内存停止通讯时,存在的当地内存数据纷歧致、编译器会对代码指令重排序、处置器会对代码乱序履行等带来的题目。

PS:

这里面提到的主内存和工作内存(高速缓存,寄存器),可以简单的类比成计较机内存模子中的主存缓和存的概念。出格需要留意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方式区等并不是同一个条理的内存分别,没法间接类比。《深入了解Java虚拟机》中以为,假如一定要委曲对应起来的话,从变量、主内存、工作内存的界说来看,主内存首要对应于Java堆中的工具实例数据部分。工作内存则对应于虚拟机栈中的部分地区。

肆意的线程之间通讯方式简单以下:

说点JMM 让你的口试如虎添翼-8.jpg

在JVM内部,Java内存模子把内存分红了两部分:线程栈区和堆区,关于JVM具体的讲授参考之前博文,这里只给出大致架构图,细节部分都写过了。

说点JMM 让你的口试如虎添翼-9.jpg

JMM带来的题目

    同享工具对各个线程的可见性

A 线程读取主内存数据点窜后还没来得及将点窜数据同步到主内存,主内存数据就又被B线程读取了。
    同享工具的合作现象

AB两个线程同时读取主内存数据,然后同时加1,再返回。

说点JMM 让你的口试如虎添翼-10.jpg

对于上面的题目不过就是变量用volatile,加锁,CAS等这样的操纵来处理。
指令重排


在履行法式时,为了进步性能,编译器和处置器经常会对指令做重排序。 比如:
code1 // 耗时10秒code2 // 耗时2秒----假如code1跟code2合适指令重拍的要求,code2不会一向等到code1履行终了再履行。复制代码
编译的源代码能够经过以下重排加速才是终极CPU履行的指令。

说点JMM 让你的口试如虎添翼-11.jpg

    编译器优化的重排序

编译器在不改变单线程法式语义的条件下,可以重新放置语句的履行顺序。
    指令级并行的重排序

现代处置器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令堆叠履行。假如不存在数据依靠性,处置器可以改变语句对应机械指令的履行顺序。
    内存系统的重排序

处置器利用缓存和读/写缓冲区,这使得加载和存储操纵看上去能够是在乱序履行(处置重视排)
数据依靠跟控制依靠


重排序对于数据依靠性跟控制依靠性的代码不会重拍。
    数据依靠 假如两个操纵拜候同一个变量,且这两个操纵中有一个为写操纵,此时这两个操纵之间就存在数据依靠性,这样的代码都不答应重排(重排后成果就纷歧样了)。数据依靠分以下三品种型:控制依靠 flag变量是个标志,用来标识变量a能否已被写入,在use方式中比变量i依靠if (flag)的判定,这里就叫控制依靠,假如发生了重排序,成果就差池了。
public void use(){   if(flag){ //A      int i = a*a;// B       ....   }}复制代码as-if-serial


不管若何重排序,都必须保证代码在单线程下的运转正确,连单线程下都没法正确,更不用会商多线程并发的情况,所以就提出了一个as-if-serial的概念, as-if-serial语义的意义是:

不管怎样重排序(编译器和处置器为了进步并行度),(单线程)法式的履行成果不能被改变。编译器、runtime和处置器都必须遵照as-if-serial语义。为了遵照as-if-serial语义,编译器和处置器不会对存在数据依靠关系的操纵做重排序,由于这类重排序会改变履行成果。(夸大一下,这里所说的数据依靠性仅针对单个处置器中履行的指令序列和单个线程中履行的操纵,分歧处置器之间和分歧线程之间的数据依靠性不被编译器和处置器斟酌)可是,假如操纵之间不存在数据依靠关系,这些操纵仍然能够被编译器和处置重视排序。
int a = 1; //1int b = 2;//2int c = a + b ;// 3复制代码
1和3之间存在数据依靠关系,同时2和3之间也存在数据依靠关系。是以在终极履行的指令序列中,3不能被重排序到1和2的前面(3排到1和2的前面,法式的成果将会被改变)。但1和2之间没稀有据依靠关系,编译器和处置器可以重排序1和2之间的履行顺序。asif-serial语义使单线程下无需担忧重排序的干扰,也无需担忧内存可见性题目
多线程下重排题目


比以下面的类中两个典范函数,假如AB线程别离同时履行分歧的函数,
    线程A对12指令重排,AB线程履行顺序为 2-3-4-1。线程B对34停止了指令重排,先读取a值为0,然后计较出a*a= 0,姑且存储下来,然后假如线程A履行终了后致使use函数里的i终极是0。
处理在并发下的题目

内存屏障


内存屏障(Memory Barrier,或偶然叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性题目。Java编译器也会按照内存屏障的法则制止重排序。 Java编译器在天生指令序列的适当位置会插入内存屏障指令来制止特定范例的处置重视排序,从而让法式按我们料想的流程去履行。

保证特定操纵的履行顺序。

影响某些数据(或则是某条指令的履行成果)的内存可见性。

编译器和CPU可以重排序指令,保证终极不异的成果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:

不管什么指令都不能和这条Memory Barrier指令重排序。

Memory Barrier所做的别的一件事是强迫刷出各类CPU cache,如一个Write-Barrier(写入屏障)将刷出一切在Barrier之前写入cache 的数据,是以,任何CPU上的线程都能读取到这些数据的最新版本。

今朝有4种屏障.。
    LoadLoad 屏障

序列:Load1,Loadload,Load2 读 读 大口语就是Load1一定要在Load2前履行,实时Load1履行慢Load2也要等Load1履行完。凡是能履行预加载指令/支持乱序处置的处置器中需要显式声明Loadload屏障,由于在这些处置器中正在期待的加载指令可以绕过正在期待存储的指令。 而对于总是能保证处置顺序的处置器上,设备该屏障相当于无操纵。
    StoreStore 屏障 写 写

序列:Store1,StoreStore,Store2 大口语就是Store1的指令任何操纵都可以实时的从高速缓存区写入到同享区,确保其他线程可以读到最新数据,可以了解为确保可见性。凡是情况下,假如处置器不能保证从写缓冲或/缓和存向别的处置器和主存中按顺序革新数据,那末它需要利用StoreStore屏障。
    LoadStore 屏障 读 写

序列: Load1; LoadStore; Store2 大致感化跟第一个类似,确保Load1的数据在Store2和后续Store指令被革新之前读取。在期待Store指令可以超出loads指令的乱序处置器上需要利用LoadStore屏障。
    StoreLoad 屏障 写 读

序列: Store1; StoreLoad; Load2 确保Store1数据对其他处置器变得可见(指革新到内存),之前于Load2及一切后续装载指令的装载。StoreLoad Barriers会使该屏障之前的一切内存拜候指令(存储和装载指令)完成以后,才履行该屏障以后的内存拜候指令。 StoreLoad Barriers是一个万能型 的屏障,它同时具有其他3个屏障的结果。现代的多处置器大多支持该屏障(其他范例的屏障纷歧定被一切处置器支持)。
临界区


也就是加锁,运转两个函数的时辰都加上不异的锁,这样就保证了两个线程履行两个函数的有序性,在同步方式里只要负责as-if-serial即可。

说点JMM 让你的口试如虎添翼-12.jpg

Happens-Before


由于有指令重排的存在会致使难以了解CPU内部运转法则,JDK用happens-before的概念来论述操纵之间的内存可见性。在JMM中,假如一个操纵履行的成果需要对另一个操纵可见,那末这两个操纵之间必必要存在happens-before关系 。其中CPU的happens-before无需任何同步手段便可以保证的。

假如一个操纵happens-before另一个操纵,那末第一个操纵的履行成果将对第二个操纵可见,而且第一个操纵的履行顺序排在第二个操纵之前。(对法式员来说)

两个操纵之间存在happens-before关系,并不意味着Java平台的具体实现必必要依照happens-before关系指定的顺序来履行。假如重排序以后的履行成果,与按happens-before关系来履行的成果分歧,那末这类重排序是答应的(对编译器和处置器 来说)

说点JMM 让你的口试如虎添翼-13.jpg

happens-before具体法则Mark下,以备不时之需。

法式顺序法则:一个线程中的每个操纵,happens-before于该线程中的肆意后续操纵。

监视器锁法则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

volatile变量法则:对一个volatile域的写,happens-before于肆意后续对这个volatile域的读。

传递性:假如A happens-before B,且B happens-before C,那末A happens-before C。

start()法则:假如线程A履行操纵ThreadB.start()(启动线程B),那末A线程的ThreadB.start()操纵happens-before于线程B中的肆意操纵。

join()法则:假如线程A履行操纵ThreadB.join()并成功返回,那末线程B中的肆意操纵happens-before于线程A从ThreadB.join()操纵成功返回。 7.线程中断法则:对线程interrupt方式的挪用happens-before于被中断线程的代码检测到中断事务的发生。
volatile语义


volatile保证变量的可见性,同时还具有弱原子性。关于volatile之前博文写细致节不再反复,指令重排的时辰对volatile法则以下:

在每个volatile写操纵的前面插入一个StoreStore屏障。在每个volatile写操纵的前面插入一个StoreLoad屏障。

在每个volatile读操纵的前面插入一个LoadLoad屏障。在每个volatile读操纵的前面插入一个LoadStore屏障
锁内存语义


有点类似于重型版本的volatile,功用以下:

当线程开释锁时,JMM会把该线程对应的当地内存中的同享变量革新到主内存中。。

当线程获得锁时,JMM会把该线程对应的当地内存置为无效。从而使得被监视器庇护的临界区代码必须从主内存中读取同享变量。
final内存语义


编译器和处置器要遵照两个重排序法则。

在机关函数内对一个final域的写入,与随后把这个被机关工具的援用赋值给一个援用变量,这两个操纵之间不能重排序。 看代码备注1

初度读一个包括final域的工具的援用,与随后初度读这个final域,这两个操纵之间不能重排序。看代码备注2
class SoWhat{    final int b;    SoWhat(){        b = 1412;    }    public static void main(String[] args) {        SoWhat soWhat = new SoWhat();        // 备注1:制止在 b = 1 这个语句履行完之前,系统将新new出来的工具地址赋值给了sowhat。        System.out.println(soWhat); //A        System.out.println(soWhat.b); //B        // 备注2: A  B 两个指令不能重排序。    }}复制代码
final为援用范例时,增加了以下法则:

在机关函数内对一个final援用的工具的成员域的写入,与随后在机关函数外把这个被机关工具的援用赋值给一个援用变量,这两个操纵之间不能重排序。
class SoWhat{    final Object b;    SoWhat(){        this.b = new Object(); //  A    }    public static void main(String[] args) {        SoWhat soWhat = new SoWhat(); //B        // 寄义是 必须A履行终了了 才可以履行B    }}复制代码
final语义在处置器中的实现

会要求编译器在final域的写以后,机关函数return之前插入一个StoreStore屏障。

读final域的重排序法则要求编译器在读final域的操纵前面插入一个LoadLoad屏障。




作者:SoWhat1412
链接:https://juejin.im/post/5ea4f5596fb9a03c6a41881a
温馨提示:
好向圈www.kuaixunai.com是各行业经验分享交流社区,你可以在这里发布交流经验,也可以发布需求与服务,经验圈子里面禁止带推广链接、联系方式、违法词等,违规将封禁账号,相关产品信息将永久不予以通过,同时有需要可以发布在自己的免费建站官网里面或者广告圈, 下载好向圈APP可以加入各行业交流群 本文不代表好向圈的观点和立场,如有侵权请下载好向圈APP联系在线客服进行核实处理。
审核说明:好向圈社区鼓励原创内容发布,如果有从别的地方拷贝复制将不予以通过,原创优质内容搜索引擎会100%收录,运营人员将严格按照上述情况进行审核,望告知!
回复

使用道具 举报

已有(1)人评论

跳转到指定楼层
含泪向前追 发表于 2021-3-23 17:07:12
转发了
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

24小时热文