JAVA内存模型简述及总结

3/8/2017来源:ASP.NET技巧人气:2999

简单定义

JMM定义了java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的 Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。原始的Java内存模型效率并不是很理想,因此Java1.5版本对其进行了重构,现在的Java8仍沿用了Java1.5的版本 主内存是线程是共享区域,每个线程都有自己的工作内存

为何要有内存模型

//我们来看这个例子 public class TestA { static int a = 0, b = 0; //定义PRivate static a,b为0, public static void main(String[] args) throws InterruptedException { while(true){ Thread threadA = new Thread(new Runnable() { public void run() { a = 2; //write 2 -> a b = 1; //write 1 -> b } }); //线程1 Thread threadB = new Thread(new Runnable() { public void run() { a = 200;//write 200 -> a b = 100;//write 100 -> b } }); //线程2 threadA.start(); threadB.start(); threadA.join(); threadB.join(); //打印a,b System.out.println("a="+a+",b="+b); } } } }

想想看上面的结果会是多少? a=2,b=1? a=200,b=100? a=2,b=100? a=200,b=1? 为什么会有这种问题? 我们在代码尾部加上

if( !((a==2 && b==1) || (a==200 && b==100)) ){ System.out.println("a="+a+",b="+b +" >>>> i="+i); break; }

输出结果: a=2,b=100 >>>> i=128348 [Finished in 42.9s]

因为ab为共享数据而两个线程在不断的对它进行读写,没有次序的,没有规则的,因此会导致数据问题,那么在内存模型中数据时如何交互的呢?

数据交互

这里写图片描述

线程A,B从主内存中获取共享数据,保存到自己的数据副本 线程B修改且刷新数据由副本到主内存 线程C获取主内存中数据 其B与C完成了线程B向线程C发送数据的过程,也就是线程间通信

例如: 我们定义了“晚餐是鱼香肉丝盖饭”在主内存中 线程B拿到这个数据保存到数据副本中 线程B将晚餐改为“晚餐是葱油拌面” 当线程C在18:00时,拿到主内存中的晚餐,得到的数据就是“葱油伴面” 从而完成了线程通信 当然你也许想到了 主内存数据同步问题。线程不安全问题,不要着急下面会说到这些

线程栈与堆

当前线程所执行方法的调用信息,2个线程栈并不相互可见。 所有基本数据类型都在线程栈中保存 (boolean/char/byte/short/int/long/float/double) heap(堆)包含了Obecjt对象,无论它是哪个线程创建的都放在了heap中 如果线程栈中包含一个对象引用那么,引用将会存在stack中而对象本身依然在heap中 static修饰的变量,类都存在heap中 这里写图片描述

同步与可见性

对于上面代码中例子的非线程安全,是因为我们没有使用volatile和synchronized进行修饰,导致共享对象的可见性,线程安全性无法得到保证,数据异常

【可见性】例子:

主存包含 i = 0 ThreadA -> read -> 主存 -> i ThreadA -> copy -> i -> 数据副本 ThreadA -> write -> i = 999 -> 数据副本 ThreadB -> read -> 主存 -> i ThreadB -> copy -> i -> 数据副本 ThreadA -> write -> 数据副本i -> 主存

ThreadB获取的 i依然为0,因为ThreadA 还没有将数据副本写回主存,ThreadA是在ThreadB之后写回的数据

最终问题在于ThraedB与ThreadA没有可见性,ThreadB 并不 happens before ThreadA

如果我们使用volatile关键字就不会发生这种状况,可以保证数据直接从主存rw,当然其中原理是基于内存屏障指令来达到的(volatile只能保证可见性,但无法保证线程安全)

【同步】例子

主存包含 i = 0 ThreadA -> read -> 主存 -> i ThreadB -> read -> 主存 -> i

ThreadA -> copy -> i -> 数据副本 ThreadB -> copy -> i -> 数据副本

ThreadA -> write -> i = i + 1 -> 数据副本 ThreadB -> write -> i =i +1 -> 数据副本

从此刻来看 i 的值应该是 2 ,其实不然,如果是串行执行,自然是 i+1 , i+1 ,但多线程中往往是并行的,那么无论是ThreadB还是A,最终主存中的值只会是2

如果我们使用sync(synchronized)关键词即可保证只能有一个线程在操作目标对象,避免线程安全问题,同时sync也保证数据的可见性(如同volatile),则数据也是从主存中直接r/w

synchronized 同步,可见性

volatile 非线程安全,可见性

指令重排 为了提高程序运行性能,jvm和cpu会对代码进行重新排序 然而内存屏障指令会禁止在特定类型修饰的目标(volition,sync)中进行指令重排,以保证可见性

编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

举个例子:

int a=1; int b=2; int c=3; Systen.out.print(a+c);

可以发现int b =2 与print并没有依赖关系并不影响 as-if-serial原则,所以int b是可以被重排的,然而 a,c的定义也是可以重排的 但是他们一定会在print前面。

as-if-serial 原则

该原则定义了在指令重排中,无论如何重排,都不可以改变原有结果

Happens Before

从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。 在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。 与程序员密切相关的happens-before规则如下: 1.程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。 2.监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。 3.volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。(内存屏障flush) 4.传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。 注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的

假定我们有已经被初始化的变量: int counter = 0; 这个 counter 变量被两个线程所共有,也就是说线程A和线程B都可以获取或者更改counter的值。 这里我们假设线程A要增加counter的值: counter++; 然后,线程B打印counter的值 System.out.println(counter); 如果上面两条语句被同一个线程执行,我们可以肯定的说打印出来的值是1. 但是如果这两条语句分别被两个线程执行,其打印出来的值却可能是0, 因为这里并没有任何保证说线程A对counter的修改一定对线程B所见。除非我们在两条语句之间建立起 happens-before的关系

参考资料: 《深入理解Java内存模型》 《全面理解Java内存模型》 Java多线程之happens-before