Java内存模型

Java内存模型

  为了屏蔽各类硬件和操做系统的内存访问差别,实现Java在不一样平台下都能达到一致的内存访问效果,而定义出的一种内存模型规范。java

1、主内存和工做内存

  Java内存模型的主要目标是为了定义程序中各个变量的访问规则(虚拟机中读写变量....这些变量包括实例字段、静态字段、构成数组对象的元素,但不包括线程私有而不存在竞争的方法参数和局部变量)。Java内存模型并无如今执行引擎使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制编译器调整代码顺序执行这一类优化措施。数组

  Java内存模型规定全部变量都存储在主内存中,除此以外,每条线程拥有本身的工做内存(线程的工做内存中保存的是执行时候须要使用的变量从主内存中拷贝的副本),且线程执行的时候对变量的读写操做都是在本身的工做内存中进行,而不是直接从主存中读或者写。(不一样的线程之间不能访问彼此的工做内存,线程之间的访问通讯均须要经过主内存来完成)缓存

  

2、内存建交互操做

  一、上面区分了主内存和工做内存两者的关系和区别,那么实际上JMM中定义了下面几种操做来完成变量从主内存拷贝到工做内存、而后从工做内存同步写回主内存中。并且这些操做都是原子性的(64位的double类型和龙类型可能会被拆分红32位来进行操做)安全

  ①lock(锁定):做用与主内存中的变量,将一个变量标识为线程独占的状态;并发

  ②unlock(解锁):做用于主内存中的变量,将一些被线程独占锁定的变量释放,从而能够被其余线程占有使用;app

  ③read(读取):做用于主内存中的变量,将变量的值从主内存中传输到工做内存中,以便后去的load操做使用;ide

  ④load(载入):做用于工做内存中的变量,将上面read操做从主内存中获取的值放在本身的工做内存中的变量副本中;函数

  ⑤use(使用):做用于工做内存中的变量,将工做内存中的变量值传递给执行引擎,当虚拟机须要使用变量的值的时候回执行这个操做;优化

  ⑥assign(赋值):做用于工做内存中的变量,将一个从执行引擎接受到的值赋给工做内存中的该变量,当虚拟机遇到给变量赋值操做的指令时候执行;this

  ⑦store(存储):做用域工做内存中的变量,将工做内存中的变量值传送回主内存中,以便后续的write操做使用;

  ⑧write(写入):做用于主内存中的变量,将store操做从工做内存中获得的变量的值写回主内存的变量中。

  二、对于一个变量而言:若是要从主内存复制到工做内存,就须要顺序执行read和load操做;若是须要将其写回主内存,就须要顺序的执行store和write操做。(这两种状况是须要按序执行的,可是不限制必须连续执行,在他们操做之间能够执行其余指令)

  三、一些其余的规则

  ①不容许read和load、store和write操做中的一个单独出现(即不容许将一个变量从主内存中读取可是工做内存不接受、或者是从工做内存写回可是主内存不接受的状况);

  ②不容许一个线程丢弃最近使用的assign操做(即变量在工做内存中改变了以后须要将变化同步回主内存之中);

  ③不容许一个线程在没有发生assign操做的时候,就将数据从工做内存同步回主内存之中;

  ④一个新的变量只能在主内存之中产生,不容许在工做内存中直接使用一个未被初始化的变量(对这个变量执行load或assign操做),即对一个变量执行use和store操做以前必须执行了assign和load操做;

  ⑤若是对一个变量进行lock操做,将会清空工做内存中该变量的值,在执行引擎使用这个变量以前,须要从新执行load和assign操做;

  ⑥若是一个变量事先没有被lock操做锁定,那么不容许对其执行unlock操做,也不容许某一个线程去unlock一个被其余线程lock住的变量;

  ⑦在对一个变量执行unlock以前,必须将这个变量的值同步回主内存中。

3、volatile变量

  一、volatile是JVM提供的轻量级同步机制 

  当一个变量被定义为volatile以后,会具有下面两种特性

  a)保证此变量对于其余全部线程的可见性(当某条线程改变了这个volatile的值后,其余的线程可以得知这个变化)。这里须要指出:

  ①volatile变量存在不一致的状况(虽然各个线程中是一致的,可是这种状况的缘由是在使用变量以前都须要刷新,致使执行引擎看不到不一致的状况,那么在各个线程中看到的天然就是一致的);

  ②Java中的运算不是原子的,致使基于volatile变量在并发状况下的操做不必定就是安全的,如同下面的例子:使用10个线程对volatile类型的变量count进行自增运算操做,而后观察运行的计算结果发现并非指望的100000,而是小于该值的某个其余值

 1 package cn.test.Volatile;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class TestVolatile02 {
 7     volatile int count = 0;
 8     void m(){
 9         count++;
10     }
11 
12     public static void main(String[] args) {
13         final TestVolatile02 t = new TestVolatile02();
14         List<Thread> threads = new ArrayList<>();
15         for(int i = 0; i < 10; i++){
16             threads.add(new Thread(new Runnable() {
17                 @Override
18                 public void run() {
19                     for(int i = 0; i < 10000; i++){
20                         t.m();
21                     }
22                 }
23             }));
24         }
25         for(Thread thread : threads){
26             thread.start();
27         }
28         for(Thread thread : threads){
29             try {
30                 thread.join();
31             } catch (InterruptedException e) {
32                 // TODO Auto-generated catch block
33                 e.printStackTrace();
34             }
35         }
36         System.out.println(t.count);
37     }
38 }

 

  ③实际上使用javap反编译以后的代码清单,咱们查看m()方法的汇编代码,发现count++实际上有四步操做:getfield->iconst_1->iadd->putfield,分析上面程序执行和预期结果不一样的缘由:getfield指令把count值取到栈顶的时候,volatile保证了count的值在此时是正确的,可是在执行iconst_1和iadd的时候,其余线程可能已经将count的值增大了,这样的话刚刚保存在操做数栈顶的值就是过时的数据了,因此最后putfield指令执行后就可能把较小的值同步到主存当中了。

  void m();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field count:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field count:I
      10: return

 

  ④上面的代码要想保证执行正确,咱们还须要在执行m方法的时候使用锁的机制来保证并发执行的正确性,可使用synchronized或者并发包下面的原子类型。下面须要使用这两种加锁机制的场合

  □运算结果不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值

  □变量不须要与其余的状态变量共同参与不变约束。

  ⑤看下面的代码,就比较适合volatile。使用volatile类型的变量b可以在其余线程调用endTest修改b的值以后,全部doTest的线程都可以停下来。

 1 package cn.test.Volatile;
 2 
 3 import java.util.concurrent.TimeUnit;
 4 
 5 public class TestVolatile01 {
 6     volatile boolean b = true;
 7 
 8     void doTest(){
 9         System.out.println("start");
10         while(b){}
11         System.out.println("end");
12     }
13 
14     void endTest() {
15         b = false;
16     }
17 
18     public static void main(String[] args) {
19         final TestVolatile01 t = new TestVolatile01();
20         new Thread(new Runnable() {
21             @Override
22             public void run() {
23                 t.doTest();
24             }
25         }).start();
26 
27         try {
28             TimeUnit.SECONDS.sleep(1);
29         } catch (InterruptedException e) {
30             e.printStackTrace();
31         }
32         t.endTest();
33     }
34 }

 

  b)禁止指令重排序优化

  普通变量只能保证在程序执行过程当中全部依赖赋值结果的地方都能获取到正确的结果,可是不能保证变量赋值操做的顺序与程序中的代码执行顺序一致。而指令的重排序就可能致使并发错误的结果。使用Volatile修饰就能够避免由于指令重排序致使的错误产生。

  c)java内存模型中对volatile变量定义的特殊规则。假定T表示一个线程,V和W分别表示volatile型变量,那么在进行read、load、use、assign、store和write操做时须要知足以下规则:

  ①只有当线程T对变量V执行的前一个动做为load时,T才能对V执行use;而且,只有T对V执行的后一个动做为use时,T才能对V执行load。T对V的use,能够认为是和T对V的load。read动做相关联,必须连续一块儿出现(这条规则要求在工做内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其余线程对V修改后的值)。

  ②只有当T对V的前一个动做是assign时,T才能对V执行store;而且,只有当T对V执行的后一个动做是store时,T才能对V执行assign。T对V的assign能够认为和T对V的store、write相关联,必须连续一块儿出现(这条规则要求在工做内存中,每次修改V后都必须马上同步回主内存中,用于保证其余线程看到本身对V的修改)。

  ③假定动做A是T对V实施的use或assign动做,假定动做F是和动做A相关联的load或store动做,假定动做P是和动做F相应的对V的read或write动做;相似的,假定动做B是T对W实施的use或assign动做,假定动做G是和动做B相关联的load或store动做,假定动做Q是和动做G相应的对W的read或write动做。若是A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令的重排序优化,保证代码的执行顺序与程序的顺序相同)。

4、原子性、可见性与有序性

  一、原子性:由Java内存模型来直接保证的原子性变量操做(包括read,load,assign,use,store,write),能够认为基本数据类型的访问读写都是具有原子性的(尽管double和long的读写操做划分为两次32位的操做来执行,可是目前商用虚拟机都将64位的数据读写操做做为原子性来对待)

  二、可见性:当一个线程修改了共享变量的值,其余线程可以当即获得这个修改的情况。除了volatile,Java还有两个关键字能实现可见性,synchronized和final。同步块的可见性是由“对一个变量执行unlock操做以前,必须把此变量同步回主内存中(执行store和write操做)”这条规则得到的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,而且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其余线程有可能经过这个引用访问到“初始化了一半”的对象),那么其余线程中就能看见final字段的值。

  三、有序性:Java提供了volatile和synchronized两个关键字来保证线程之间操做的有序性,volatile关键字自己就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只容许一条线程对其进行lock操做”这条规则得到的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

5、先行发生原则(happens-before)

  一、先行发生原则:是Java内存模型中定义的两项操做之间的偏序关系,若是说操做A发生在操做B以前,那么操做A产生的结果能被操做B观察到(这个结果包括修改内存中共享变量的值、发送通讯消息、调用某个方法等等)。

  二、下面是Java内存模型中的一些先行发生关系,这些happens-before关系不须要任何的同步操做就已经存在,能够在编码中直接使用,若是两个操做之间的关系不在此列,而且没法从下列规则中推导出来的话,他们就没有顺序性保证,那么虚拟机就能够对其进行重排序优化。

  ①程序次序规则:在一个线程内,按照程序控制流(包括分支、循环)顺序执行。

  ②管程锁定规则:一个unlock操做先行发生于后面对同一个锁的lock操做。

  ③volatile变量规则:对一个volatile变量的写操做先行发生于后面对这个变量的读操做。

  ④线程启动规则:Thread对象的start方法先行发生于此线程的每一个动做。

  ⑤线程终止规则:线程中的全部操做都先行发生于此线程的终止检测,咱们能够经过Thread.join()方法结束/Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  ⑥线程终端规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过Thread.interrupted()方法检测到是否有中断发生。

  ⑦对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize方法的开始、

  ⑧传递性:若是操做A先行发生于操做B,操做B先行发生于操做C,那么操做A先行发生于操做C。

相关文章
相关标签/搜索