Java内存模型
为了屏蔽各种硬件和操作系统的内存访问差异,实现Java在不同平台下都能达到一致的内存访问效果,而定义出的一种内存模型规范。
一、主内存和工作内存
Java内存模型的主要目标是为了定义程序中各个变量的访问规则(虚拟机中读写变量....这些变量包括实例字段、静态字段、构成数组对象的元素,但不包括线程私有而不存在竞争的方法参数和局部变量)。Java内存模型并没有现在执行引擎使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制编译器调整代码顺序执行这一类优化措施。
Java内存模型规定所有变量都存储在主内存中,除此之外,每条线程拥有自己的工作内存(线程的工作内存中保存的是执行时候需要使用的变量从主内存中拷贝的副本),且线程执行的时候对变量的读写操作都是在自己的工作内存中进行,而不是直接从主存中读或者写。(不同的线程之间不能访问彼此的工作内存,线程之间的访问通信均需要通过主内存来完成)
二、内存建交互操作
1、上面区分了主内存和工作内存二者的关系和区别,那么实际上JMM中定义了下面几种操作来完成变量从主内存拷贝到工作内存、然后从工作内存同步写回主内存中。而且这些操作都是原子性的(64位的double类型和龙类型可能会被拆分成32位来进行操作)
①lock(锁定):作用与主内存中的变量,将一个变量标识为线程独占的状态;
②unlock(解锁):作用于主内存中的变量,将一些被线程独占锁定的变量释放,从而可以被其他线程占有使用;
③read(读取):作用于主内存中的变量,将变量的值从主内存中传输到工作内存中,以便后去的load操作使用;
④load(载入):作用于工作内存中的变量,将上面read操作从主内存中获取的值放在自己的工作内存中的变量副本中;
⑤use(使用):作用于工作内存中的变量,将工作内存中的变量值传递给执行引擎,当虚拟机需要使用变量的值的时候回执行这个操作;
⑥assign(赋值):作用于工作内存中的变量,将一个从执行引擎接受到的值赋给工作内存中的该变量,当虚拟机遇到给变量赋值操作的指令时候执行;
⑦store(存储):作用域工作内存中的变量,将工作内存中的变量值传送回主内存中,以便后续的write操作使用;
⑧write(写入):作用于主内存中的变量,将store操作从工作内存中得到的变量的值写回主内存的变量中。
2、对于一个变量而言:如果要从主内存复制到工作内存,就需要顺序执行read和load操作;如果需要将其写回主内存,就需要顺序的执行store和write操作。(这两种情况是需要按序执行的,但是不限制必须连续执行,在他们操作之间可以执行其他指令)
3、一些其他的规则
①不允许read和load、store和write操作中的一个单独出现(即不允许将一个变量从主内存中读取但是工作内存不接受、或者是从工作内存写回但是主内存不接受的情况);
②不允许一个线程丢弃最近使用的assign操作(即变量在工作内存中改变了之后需要将变化同步回主内存之中);
③不允许一个线程在没有发生assign操作的时候,就将数据从工作内存同步回主内存之中;
④一个新的变量只能在主内存之中产生,不允许在工作内存中直接使用一个未被初始化的变量(对这个变量执行load或assign操作),即对一个变量执行use和store操作之前必须执行了assign和load操作;
⑤如果对一个变量进行lock操作,将会清空工作内存中该变量的值,在执行引擎使用这个变量之前,需要重新执行load和assign操作;
⑥如果一个变量事先没有被lock操作锁定,那么不允许对其执行unlock操作,也不允许某一个线程去unlock一个被其他线程lock住的变量;
⑦在对一个变量执行unlock之前,必须将这个变量的值同步回主内存中。
三、volatile变量
1、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修饰的变量不会被指令的重排序优化,保证代码的执行顺序与程序的顺序相同)。
四、原子性、可见性与有序性
1、原子性:由Java内存模型来直接保证的原子性变量操作(包括read,load,assign,use,store,write),可以认为基本数据类型的访问读写都是具备原子性的(尽管double和long的读写操作划分为两次32位的操作来执行,但是目前商用虚拟机都将64位的数据读写操作作为原子性来对待)
2、可见性:当一个线程修改了共享变量的值,其他线程能够立即得到这个修改的状况。除了volatile,Java还有两个关键字能实现可见性,synchronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行store和write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么其他线程中就能看见final字段的值。
3、有序性:Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
五、先行发生原则(happens-before)
1、先行发生原则:是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A发生在操作B之前,那么操作A产生的结果能被操作B观察到(这个结果包括修改内存中共享变量的值、发送通信消息、调用某个方法等等)。
2、下面是Java内存模型中的一些先行发生关系,这些happens-before关系不需要任何的同步操作就已经存在,可以在编码中直接使用,如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来的话,他们就没有顺序性保证,那么虚拟机就可以对其进行重排序优化。
①程序次序规则:在一个线程内,按照程序控制流(包括分支、循环)顺序执行。
②管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
③volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
④线程启动规则:Thread对象的start方法先行发生于此线程的每个动作。
⑤线程终止规则:线程中的所有操作都先行发生于此线程的终止检测,我们可以通过Thread.join()方法结束/Thread.isAlive()的返回值等手段检测到线程已经终止执行。
⑥线程终端规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
⑦对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize方法的开始、
⑧传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。