JVM学习(3)——总结Java内存模型

俗话说,本身写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结以下:java

  • 为何学习Java的内存模式
  • 缓存一致性问题
  • 什么是内存模型
  • JMM(Java Memory Model)简介
  • volatitle关键字
  • 原子性
  • 可见性
  • 有序性
  • 指令重排
  • 先行发生——happen-before原则
  • 解释执行和编译执行
  • 其余语言(c和c++)也有内存模型么?

 


  为何须要关注Java内存模型?c++

 
  以前有一个我实习的同事(已经工做的)反讽我:学(关注)这个有什么用?
  我没有回答,我牢记一句话: 大天苍苍兮大地茫茫,人各有志兮何可思量。 我只知道并发程序的bug很是难找。它们经常不会在测试中发现,而是直到程序运行在高负荷的状况下或者长期运行以后才发生,可是那时候再修复的代价是很大的,且也很是难于重现和跟踪。故开发,维护人员须要花费比以前更多的努力,去提早保证程序是正确同步的。而这不容易,可是它比前者——调试一个没有正确同步的程序要容易的多。
  本文确定不会,也不可能全面深刻的总结完每一个Java内存模型的知识点,只是做为熟悉JVM的内存模型,而内部的一些具体的原理和细节,以后开专题总结之。
 
   缓存一致性问题

  众所周知,计算机某个运算的完成不只仅依靠cpu及其寄存器,还要和内存交互!cpu须要读取内存中的运行数据,存储运算结果到内存中……其中很天然的也是没法避免的就涉及到了I/O操做,而常识告诉咱们,I/O操做和cpu的运算速度比起来,简直没得比!前者远远慢于后者(书上说相差几个数量级!),前面JVM学习2也总结了这个情景,人们解决的方案是加缓存——cache(高速缓存),cache的读写速度尽量的接近cpu运算速度,来做为内存和cpu之间的缓冲!旧的问题解决了,可是引起了新的问题!若是有多个cpu怎么办?程序员

  现代操做系统都是多核心了,若是多个cpu和一块内存进行交互,那么每一个cpu都有本身的高速缓存块……咋办?也就是说,多个cpu的运算都访问了同一块内存块的话,可能致使各个cpu的缓存数据不一致!if发生了上述情景,then以哪一个cpu的缓存为主呢?为了解决这个问题,人们想到,让各个cpu在访问缓存时都遵循某事先些规定的协议!由于无规矩不成方圆!如图(如今能够回答什么是内存模型了):编程

  什么是内存模型?数组

  通俗的说,就是在某些事先规定的访问协议约束下,计算机处理器对内存或者高速缓存的访问过程的一种抽象!这是物理机下的东西,其实对虚拟机来讲(JVM),道理是同样的!缓存

 

  什么是Java的内存模型(JMM)?安全

  教科书这样写的:JVM规范说,Java程序在各个os平台下必须实现一次编译,处处运行的效果!故JVM规范定义了一个模型来屏蔽掉各种硬件和os之间内存访问的差别(好比Java的并发程序必须在不一样的os下运行效果是一致的)!这个模型就是Java的内存模型!简称JMM。多线程

  让我通俗的说:Java内存模型定义了把JVM中的变量存储到内存和从内存中读取出变量的访问规则,这里的变量不算Java栈内的局部变量,由于Java栈是线程私有的,不存在共享问题。细节上讲,JVM中有一块主内存不是彻底对应物理机主内存的那个概念,这里说的JVM的主内存是JVM的一部分,它主要对应Java堆中的对象实例及其相关信息的存储部分)存储了Java的全部变量。且Java的每个线程都有一个工做内存对应Java栈),里面存放了JVM主内存中变量的值的拷贝!且Java线程的工做内存和JVM的主内存独立!如图:架构

  当数据从JVM的主内存复制一份拷贝到Java线程的工做内存存储时,必须出现两个动做:并发

  1. 由JVM主内存执行的读(read)操做
  2. 由Java线程的工做内存执行相应的load操做

  反过来,当数据从线程的工做内存拷贝到JVM的主内存时,也出现两个操做:

  1. 由Java线程的工做内存执行的存储(store)操做;
  2. 由JVM主内存执行的相应的写(write)操做

  read,load,store,write的操做都是原子的,即执行期间不会被中断!可是各个原子操做之间可能会发生中断对于普通变量,若是一个线程中那份JVM主内存变量值的拷贝更新了,并不能立刻反应在其余变量中,由于Java的每一个线程都私有一个工做内存,里面存储了该条线程须要用到的JVM主内存中的变量拷贝!(好比实例的字段信息,类型的静态变量,数组,对象……)如图:

A,B两条线程直接读or写的都是线程的工做内存!而A、B使用的数据从各自的工做内存传递到同一块JVM主内存的这个过程是有时差的,或者说是有隔离的!通俗的说他们之间看不见!也就是以前说的一个线程中的变量被修改了,是没法当即让其余线程看见的!若是须要在其余线程中当即可见,须要使用 volatile 关键字。如今引出volatile关键字:

 


 

  volatile 关键字是干吗的?举例说明。

  前面说了,各个线程之间的变量更新,若是想让其余线程当即可见,那么须要使用它,故volatile字段是用于线程间通信的特殊字段。每次读volatile字段都会看到其它线程写入该字段的最新值!也就是说,一旦一个共享变量(成员、静态)被volatile修饰,那么就意味着:a线程修改了该变量的值,则这个新的值对其余线程来讲,是当即可见的!先看一个例子:

  这段代码会彻底运行正确么?即必定会中断么?

 

//线程A
boolean stop = false;

while(!stop){
    doSomething();
}
 
//=========
//线程B
stop = true;
View Code

 

  有些人在写程序时,若是须要中断线程,可能都会采用这种办法。可是这样作是有bug的!虽然这个可能性很小,可是只要一旦bug发生,后果很严重!前面已经说了,Java的每一个线程在运行过程当中都有本身的工做内存,且Java的并发模型采用的是共享内存模型,Java线程之间的通讯老是隐式进行,整个通讯过程对程序员彻底透明,这也是为何若是编写多线程程序的Java程序员不理解隐式进行的线程之间通讯的工做机制,则极可能会遇到各类奇怪的并发问题的缘由。针对本题的A、B线程,若是他们之间通讯,画成图是这样的:

那么线程A和B须要通讯的时候,第一步A线程会将本地工做内存中的stop变量的值刷新到JVM主内存中,主内存的stop变量=false,第二步,线程B再去主内存中读取stop的拷贝,临时存储在B,此时B中工做内存的stop也为false了。当线程B更改了stop变量的值为true以后,一样也须要作相似线程A那样的工做……可是此时此刻,偏偏B还没来得及把更新以后的stop写入主存当中(前面说了各个原子操做之间能够中断),就转去作其余事情了,那么线程A因为不知道线程B对stop变量的更改,所以还会一直循环下去。这就是死循环的潜在bug!

  从总体来看,这两个步骤实质上是线程A在向线程B发送消息,并且这个通讯过程必需要通过主内存。JMM经过控制主内存与每一个线程的工做内存之间的交互,来为java程序员提供内存可见性保证。可是它们之间不是当即可见的

  若是stop使用了volatile修饰,会使得:

  • B线程更新stop值为true,会强制将修改后的值当即写入JVM主内存,不准原子操做之间中断。
  • 线程B修改stop时,也会让线程A的工做内存中的stop缓存行失效!由于A线程的工做内存中JVM主内存的stop的拷贝值缓存行无效了,因此A线程再次读取stop的值会去JVM主内存读取

这样A获得的就是最新的正确的stop值——true。程序完美的实现了中断。不少人还认为,volatile这么好,它比锁的性能好多了!其实这不是绝对的,很片面,只能说volatile比重量级的锁(Java中线程是映射到操做系统的原生线程上的,若是要唤醒或者是阻塞一条线程须要操做系统的帮忙,这就须要从用户态转换到核心态,而状态转换须要至关长的时间……因此说syncronized关键字是java中比较重量级的操做)性能好,并且valatile万万不能代替锁,由于它不是线程安全的,既volatile修饰符没法保证对变量的任何操做都是原子的!鉴于主要涉及了Java的并发编程,以后再开专题总结)。

  

  什么是原子性?

  在Java中,对基本数据类型的变量的操做是原子性操做,即这些操做是不可被中断的,要么执行,要么不执行。看例子:

1 int x = 10;         //语句1
2 y = x;                //语句2
3 x++;                 //语句3
4 x = x + 1;         //语句4
View Code

  这几个语句哪一个是原子操做?

 

  其实只有语句1是原子性操做,其余三个语句都不是原子性操做。语句1是直接将数值10赋值给x,也就是说线程执行这个语句会直接将数值10写入到工做内存中。线程执行语句2实际上包含2个操做,它先要去主内存读取x的值,再将x的值写入工做内存,虽然读取x的值以及将x的值写入工做内存这2个操做都是原子性操做,可是合起来就不是原子性操做了。一样的,x++和 x = x+1包括3个操做:读取x的值,进行加1操做,写入新的值。因此上面4个语句只有语句1的操做具有原子性。也就是说,只有简单的读取、赋值(并且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操做)才是原子操做。

  不过这里有一点须要注意:在32位平台下,对64位数据的读取和赋值是须要经过两个操做来完成的,不能保证其原子性。可是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操做了。从上面能够看出,Java内存模型只保证了基本读取和赋值是原子性操做,若是要实现更大范围操做的原子性,能够经过synchronized和Lock来实现。因为synchronized和Lock可以保证任一时刻只有一个线程执行该代码块,那么天然就不存在原子性问题了,从而保证了原子性。

 

  什么时候使用volatile关键字?

     一般来讲,使用volatile必须具有如下2个条件:

  • 对变量的写操做不依赖于当前值
  • 该变量没有包含在具备其余变量的不变式中

这些条件代表,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。个人理解就是上面的2个条件须要保证操做是原子性操做,才能保证使用volatile关键字的程序在并发时可以正确执行。好比boolean类型的标记变量。

  前面只是大概总结了下Java的内存模式和volatile关键字,不是很深刻,留待后续并发专题补充。下面接着看几个以前和以后会遇到的概念:

  

  到底什么是可见性?如何保证?

  大白话就是一个线程修改了变量,其余线程能够当即可以知道。保证可见性可使用以前提到的volatile关键字(强制当即写入主内存,使得其余线程共享变量缓存行失效),还有重量级锁 synchronized (也就是线程间的同步,unlock以前,写变量值回主存,看做顺序执行的),最后就是常量—— final修饰的(一旦初始化完成,其余线程就可见)。其实这里忍不住仍是补充下,关键字volatile 的语义除了保证不一样线程对共享变量操做的可见性,还能禁止进行指令重排序!也就是保证有序性。这样又引出一个问题:
 
  什么是有序性和重排序?
  仍是大白话, 在本线程内,全部的操做看起来都是有序的,可是在本线程以外(其余线程)观察,这些操做都是无序的。涉及到了:
  • 指令重排(破坏线程间的有序性)
  • 以前说的工做内存和主内存同步延时(也就是线程A前后更新两个变量m和n,可是因为线程工做内存和JVM主内存之间的同步延时,线程B可能还没彻底同步线程A更新的两个变量,可能先看到了n……对于B来讲,它看A的操做就是无序的,顺序没法保证)。

 

  谈谈对指令重排的理解

   要知道,编译器和处理器会尽量的让程序的执行性能更优越!为此,他们会对一些指令作一些优化性的顺序调整! 好比有这样一个可重排语句:
a=1;
b=2;
View Code

先给a赋值,和先给b赋值,其实没什么区别,效果是同样的,这样的代码就是可重排代码,编译器会针对上下文对指令作顺序调整,哪一个顺序好,就用哪一个,因此实际上两句话怎么个执行顺序,是不必定的。

  有可重排就天然会有不可重排,首先要知道Java内存模型具有一些先天的“有序性”,即不须要经过任何手段就可以保证有序性,这个一般也称为 happens-before 原则。若是两个操做的执行次序没法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机能够随意地对它们进行重排序。反之遵循了happen-before原则,JVM就没法对指令进行重排序(看起来的)。这样又引出了一个新问题:

 

  什么是先行发生原则happens-before?

  下面就来具体介绍下happens-before(先行发生原则,这里的先行和时间上先行是两码事;):

  • 程序次序规则在一个线程内,书写在前面的操做先行发生于书写在后面的操做,就像刚刚说的,一段代码的执行在单个线程中看起来是有序的,程序看起来执行的顺序是按照代码顺序执行的,由于虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,可是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。所以,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但没法保证程序在多线程中执行的正确性。
  • 锁定规则:一个unLock操做先行发生于后面对同一个锁的lock操做,也就是说不管在单线程中仍是多线程中,同一个锁若是出于被锁定的状态,那么必须先对锁进行了释放操做,后面才能继续进行lock操做。
  • volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做,这是一条比较重要的规则。就是说若是一个线程先去写一个volatile变量,而后另外一个线程去读取,那么写入操做确定会先行发生于读操做。
  • 传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C,实际上就是体现happens-before原则具有传递性。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个一个动做
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中全部的操做都先行发生于线程的终止检测,Thread.join()。
  • 对象终结规则:一个对象的初始化完成(构造器执行结束)先行发生于他的finalize()方法的开始

  前4条规则是比较重要的,后4条规则都是常识。

  好比像以下这样的线程内的串行语义()是不可重排语句:

  • 写后读   
a = 1;
b = a;// 写一个变量以后,再读这个变量
  • 写后写  
a = 1;
a = 2;  // 写一个变量以后,再写这个变量。
  • 读后写  
a = b;
b = 1; //  读一个变量以后,再写这个变量。

以上语句不可重排,单线程的程序看起来执行的顺序是按照代码顺序执行的,这句话要正确理解:JVM实际上仍是可能会对程序代码不存在数据依赖性的指令进行指令重排序,虽然进行重排序,可是最终执行的结果是与单线程的程序顺序执行的结果一致的。所以,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但没法保证程序在多线程中执行的正确性。对于多线程环境,编译器不考虑多线程间的语义。看一个例子:

 1 class OrderExample {
 2     private int a = 0;
 3 
 4     private boolean flag = false;
 5 
 6     public void writer() {
 7         a = 1;
 8         flag = true;
 9     }
10 
11     public void reader() {
12         if (flag) {
13             int i =  a + 1;
14         }
15     }
16 }
View Code

让线程A首先执行writer()方法,接着让线程B线程执行reader()方法,线程B若是看到了flag,那么就可能会当即进入if语句,可是在int i=a+1处不必定能看到a已经被赋值为1,由于在writer中,两句话顺序可能打乱!有可能对于B线程,它看A是无序的!编译器没法保证有序性。由于A彻底能够先执行flag=true,再执行a=1,不影响结果!如图:

  也就是说多线程之间没法保证指令的有序性!先行发生原则的程序次序有序性原则是针对单线程的。也就是说,若是是一个线程去前后执行这两个方法,彻底是ok的!符合happens-before原则的第一条——程序次序有序性,故不存在指令重排问题。

  如何解决呢?仍是套用先行发生原则,看第二条锁定原则,咱们可使用同步锁:

class OrderExample {
    private int a = 0;

    private boolean flag = false;

    public synchronized void writer() {
        a = 1;
        flag = true;
    }

    public synchronized void reader() {
        if (flag) {
            int i = a + 1;
        }
    }
}
View Code

由于写、读都加锁了,他们之间本质是串行的,即便线程A占有写锁期间,JVM对写作了指令重排也不要紧,由于此时锁被A拿了,B线程没法执行读操做,直到A线程把写操做执行完毕,释放了该锁,B线程才能拿到这同一个对象锁,而此时,a确定是1,flag也必然是true了。此时必然是有序的。通俗的说,同步后,即便作了重排,由于互斥的缘故,reader 线程看writer线程也是顺序执行的。

 

 

  其余语言(c和c++)也有内存模型么?

  大部分其余的语言,像C和C++,都没有被设计成直接支持多线程。这些语言对于发生在编译器和处理器平台架构的重排序行为的保护机制会严重的依赖于程序中所使用的线程库(例如pthreads),编译器,以及代码所运行的平台所提供的保障。


 

  最后补充下一个问题:Java的字节码两种运行方式——解释执行和编译执行

  • 解释运行:解释执行以解释方式运行字节码,解释执行的意思是:读一句执行一句。
  • 编译运行(JIT):将字节码编译成机器码,直接执行机器码,是在运行时编译(不是代码写完了编译的),编译后性能有数量级的提高(能差10倍以上)
相关文章
相关标签/搜索