当前位置: 首页 > Web前端 > HTML5

Java内存模型VolatileandSynchronized

时间:2023-04-05 01:32:33 HTML5

共享内存模型指的是Java内存模型(简称JMM)。JMM确定当一个线程写入共享变量时,它可以被另一个线程看到。JMM从抽象的角度定义了线程与主存的抽象关系:线程间的共享变量存储在主存(mainmemory)中,每个线程都有一个私有的本地内存(localmemory),线程的一份副本读/写共享变量存储在本地内存中。本地内存是JMM的一个抽象概念,并不真正存在。它涵盖了高速缓存、写入缓冲区、寄存器以及其他硬件和编译器优化。从上图可以看出,线程A和线程B要进行通信,必须经过以下两个步骤:首先,线程A将本地内存A中更新的共享变量刷新到主存中。在这里,小编建了一个前端学习交流按钮群:132667127,自己整理的最新前端资料和进阶开发教程。如果愿意,可以进群一起学习交流。然后,线程B去主存中读取线程A之前更新过的共享变量。下图说明了这两个步骤:如上图所示,本地内存A和B在主内存中拥有共享变量x的副本。假设一开始,这三个内存中的x值都为0,线程A在执行时,将更新后的x值(假设为1)暂存在自己的本地内存A中,当线程A和线程B需要时为了进行通信,线程A会先将自己本地内存中修改后的x值刷新到主存中,此时主存中的x值变为1,随后线程B去主存中读取更新后的x值线程A,此时线程B的本地内存的x值也变成了1。从整体上看,这两步本质上就是线程A向线程B发送消息,而这个通信过程必须经过主存。JMM通过控制主内存和每个线程的本地内存之间的交互,为Java程序员提供内存可见性保证。总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存储在主内存中,每个线程都有自己的本地内存。当多个线程同时访问一段数据时,本地内存可能没有及时刷新到主内存,就会出现线程安全问题。Volatile什么是Volatile可见性?也就是说,一旦一个线程修改了一个被volatile修饰的变量,它会保证修改后的值会立即更新到主存中。当其他线程需要读取时,可以立即获取修改后的值。价值。Java中为了加快程序的运行效率,对一些变量的操作通常是在线程的寄存器或者CPU缓存中进行,然后同步到主存,而带有volatile修饰符的变量则直接读写主存。Volatile保证线程间共享变量的及时可见性,但不能保证原子性classThreadVolatileDemoextendsThread{publicbooleanflag=true;@Overridepublicvoidrun(){System.out.println("开始执行子线程...");while(flag){}System.out.println("线程停止");}publicvoidsetRuning(booleanflag){this.flag=flag;}}publicclassThreadVolatile{publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadVolatileDemothreadVolatileDemo=newThreadVolatileDemo();threadVolatileDemo.start();线程.睡眠(3000);threadVolatileDemo.setRuning(false);System.out.println("标志已设置为假");线程.sleep(1000);System.out.println(threadVolatileDemo.flag);}}运行结果:结果已经设置为fasle为什么?仍在运行。原因:线程之间是不可见的,读到的是一份副本,没有及时读到主存的结果。解决方法是用Volatile关键字解决线程间的可见性,强制线程每次读取值都要去“主存”取值Volatile特性1.保证这个变量对所有线程的可见性,这里的“可见性”,正如本文开头提到的,当一个线程修改这个变量的值时,volatile保证新的值可以立即同步到主存,并且在每次使用前立即从主存中刷新。但是普通变量做不到这一点。普通变量的值需要通过主存(参见:Java内存模型)来完成线程间的值传递。2.禁用指令重新排序优化。对于用volatile修饰的变量,赋值后会额外执行一次“loadaddl$0x0,(%esp)”操作,相当于内存屏障(重新排序指令时,后面的指令不能重新排序到内存屏障之前的位置)),当只有一个CPU访问内存时,不需要内存屏障;(什么是指令重排序:是指CPU采用的方法,让多条指令按照程序规定的顺序分别发送给相应的电路单元进行处理)。volatile性能:  volatile的读性能消耗和普通变量几乎一样,但是写操作稍微慢一些,因为它需要在native代码中插入很多内存屏障指令来保证处理器不会执行出来秩序。Volatile和Synchronized的区别(一)所以我们可以看出volatile虽然有可见性,但是不能保证原子性。(2)在性能方面,synchronized关键字是为了防止多个线程同时执行一段代码,影响程序执行效率,而volatile关键字在某些情况下比synchronized具有更好的性能。但是要注意volatile关键字不能代替synchronized关键字,因为volatile关键字不能保证操作的原子性。重新排序数据依赖性如果两个操作访问同一个变量,并且两个操作之一是写操作,则两个操作之间存在数据依赖性。数据依赖分为以下三种:在以上三种情况下,只要将两个操作的执行顺序重新排序,就会改变程序的执行结果。如前所述,编译器和处理器可能会重新排序操作。编译器和处理器在重新排序时会尊重数据依赖性,并且编译器和处理器不会更改具有数据依赖性的两个操作的执行顺序。注意,这里所说的数据依赖只是针对在单个处理器中执行的指令序列和在单个线程中执行的操作,编译器和处理器并没有考虑不同处理器和不同线程之间的数据依赖。as-if-serialsemanticss-if-serialsemantics的意思是:无论怎么重新排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果都不会改变。编译器、运行时和处理器都必须遵守似串行的语义。为了符合as-if-serial语义,编译器和处理器不会对有数据依赖的操作重新排序,因为这样的重新排序会改变执行结果。但是,如果这些操作之间不存在数据依赖性,则编译器和处理器可能会对其进行重新排序。具体解释请看下面计算圆面积的代码示例:doublepi=3.14;//双r=1.0;//Bdoublearea=pi*r*r;//以上三个操作在C中的数据依赖如下图所示:如上图所示,A和C之间存在数据依赖,B和C之间也存在数据依赖。因此,在最终执行的指令序列中,C不能重新排到A和B的前面(C放在A和B的前面,程序的结果会改变)。但是A和B之间没有数据依赖,编译器和处理器可以重新排序A和B之间的执行顺序。下图是程序的两个执行顺序:as-if-serial语义保护了单线程program,以及符合as-if-serial语义的编译器,runtime和processor是共同创造出来的,为编写单线程程序的程序员创造了一种错觉:单线程程序是按照程序的先后顺序执行的。as-if-serial语义使单线程程序员不必担心重新排序会干扰他们或内存可见性问题。程序顺序规则根据happens-before的程序顺序规则,上述计算圆面积的示例代码中存在三种happens-before关系:Ahappens-beforeB;B发生在C之前;A发生在C之前;第三个这里的happens-before关系是从happens-before的传递性推导出来的。这里Ahappens-beforeB,但在实际执行中,B可以先于A执行(参见上面重新排序的执行顺序)。如第一章所述,如果Ahappens-beforeB,JMM并不要求A必须在B之前执行。JMM只要求前一个操作(执行的结果)对后一个操作可见,并且前一个操作顺序在第二个操作之前。这里,操作A的执行结果不需要对操作B可见;将操作A和操作B重新排序后的执行结果与happens-before顺序中的操作A和操作B的执行结果一致。在这种情况下,JMM会认为这次重新排序不是非法的(notillegal),JMM允许这次重新排序。在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的情况下,尽可能地发展并行性。编译器和处理器遵循这个目标,我们从happens-before的定义中可以看出,JMM也遵循这个目标。重新排序对多线程的影响现在让我们看看重新排序是否会改变多线程程序的执行结果。请看下面的示例代码:classReorderExample{inta=0;booleanflag=false;publicvoidwriter(){a=1;//1标志=真;//2}Publicvoidreader(){if(flag){//3inti=a*a;//4...}}}标志变量是用来标识变量a是否已经写入的标志。这里假设有两个线程A和B,A先执行writer()方法,然后B线程再执行reader()方法。线程B在执行操作4时,线程A能看到操作1中共享变量a的写入吗?答案是:不一定可见。由于操作1和操作2没有数据依赖性,编译器和处理器可以重新排序这两个操作;同样,操作3和操作4没有数据依赖性,编译器和处理器也可以对这两个操作进行重新排序。我们先看看操作1和操作2重新排序后会发生什么?请看下面的程序执行时序图:如上图所示,操作1和操作2已经重新排序。程序执行时,线程A先写入标记变量flag,然后线程B读取这个变量。由于条件的计算结果为真,线程B将读取变量a。这个时候变量a根本就没有被线程A写过,这里多线程程序的语义被重排序破坏了!※注:本文中红色虚线箭头表示错误的读操作,绿色虚线箭头表示正确的读操作。接下来,让我们看看操作3和4重新排序时会发生什么(借助于此重新排序,我们可以顺便解释一下控制依赖项)。下面是操作3和操作4重新排序后的程序执行时序图:在程序中,操作3和操作4之间存在控制依赖关系,当代码中存在控制依赖关系时,影响的程度指令序列执行中的并行性。为此,编译器和处理器会使用推测(Speculation)执行来克服控制依赖对并行性的影响。以处理器的推测执行为例,执行线程B的处理器可以预先读取并计算a*a,然后将计算结果暂时保存在一个叫做reorderbuffer(ROB)的硬件缓存中。当判断下一个操作3的条件为真时,将计算结果写入变量i。我们从图中可以看出,猜测执行本质上是对操作3和4进行了重新排序。重新排序在这里破坏了多线程程序的语义!在单线程程序中,有控制依赖的操作重排序不会改变执行结果(这就是为什么as-if-serial语义允许有控制依赖的操作重排序);但是在多线程程序中,Reordering操作有控制依赖可能会改变程序的执行结果。