Java 内存模型跟上一篇 JVM 内存结构很像,我常常会把他们搞混,但其实它们不是一回事,并且相差还很大的,但愿你没它们搞混,特别是在面试的时候,搞混了的话就会答非所问,影响你的面试成绩,固然也许你碰到了半吊子面试官,那就要恭喜你了。Java 内存模型比 JVM 内存结构复杂不少,Java 内存模型有一个规范叫:《JSR 133 :Java内存模型与线程规范》,里面的内容很丰富,若是你没看过的话,我建议你看一下。今天咱们就简单的来聊一聊 Java 内存模型,关于 Java 内存模型,咱们仍是先从硬件内存模型入手。java
先来看看硬件内存简单架构,以下图所示:程序员
这是一幅简单的硬件内存结构图,真实的结构图要比这复杂不少,特别是在缓存层,如今的计算机中 CPU 缓存通常有三层,你也能够打开你的电脑看看,打开 任务资源管理器 ---> 性能 ---> cpu ,以下图所示:面试
从图中能够看出我这台机器的 CPU 有三级缓存,一级缓存 (L1) 、二级缓存(L2)、三级缓存(L3),一级缓存是最接近 CPU 的,三级缓存是最接近内存的,每一级缓存的数据都是下一级缓存的一部分。三级缓存架构以下图所示:redis
如今咱们对硬件内存架构有了必定的了解,咱们来弄明白一个问题,为何须要在 CPU 和内存之间添加缓存?编程
关于这个问题咱们就简单点说,咱们知道 CPU 是高速的,而内存相对来讲是低速的,这就会形成一个问题,不能充分的利用 CPU 高速的特色,由于 CPU 每次从内存里获取数据的话都须要等待,这样就浪费了 CPU 高速的性能,缓存的出现就是用来消除 CPU 与内存之间差距的。缓存的速度要大于内存小于 CPU ,加入缓存以后,CPU 直接从缓存中读取数据,由于缓存仍是比较快的,因此这样就充分利用了 CPU 高速的特性。但也不是每次都能从缓存中读取到数据,这个跟咱们项目中使用的 redis 等缓存工具同样,也存在一个缓存命中率,在 CPU 中,先查找 L1 Cache,若是 L1 Cache 没有命中,就往 L2 Cache 里继续找,依此类推,最后没找到的话直接从内存中取,而后添加到缓存中。固然当 CPU 须要写数据到主存时,一样会先刷新寄存器中的数据到 CPU 缓存,而后再把数据刷新到主内存中。数组
也许你已经看出了这个框架的弊端,在单核时代只有一个处理器核心,读/写操做彻底都是由单核完成,没什么问题;可是多核架构,一个核修改主存后,其余核心并不知道数据已经失效,继续傻傻的使用主存或者本身缓存层的数据,那么就会致使数据不一致的状况。关于这个问题 CPU 硬件厂商也提供了解决办法,叫作缓存一致性协议(MESI协议),缓存一致性协议这东西我也不了解,我也说不清,因此就不在这里 BB 了,有兴趣的能够自行研究。缓存
聊完了硬件内存架构,咱们将焦点回到咱们的主题 Java 内存模型上,下面就一块儿来聊一聊 Java 内存模型。微信
Java 内存模型是什么?Java 内存模型能够理解为遵守多核硬件架构的设计,用 Java 实现了一套 JVM 层面的“缓存一致性”,这样就能够规避 CPU 硬件厂商的标准不同带来的风险。好了,正式介绍一下 Java 内存模型:Java 内存模型 ( Java Memory Model,简称 JMM ),自己是种抽象的概念,并非像硬件架构同样真实存在的,它描述的是一组规则或规范,经过这组规范定义了程序中各个变量 (包括实例字段、静态字段和构成数组对象的元素) 的访问方式,更多关于 Java 内存模型知识能够阅读 JSR 133 :Java内存模型与线程规范。网络
咱们知道 JVM 运行程序的实体是线程,在上一篇 JVM 内存结构中咱们得知每一个线程建立时,JVM 都会为其建立一个工做内存 ( Java 栈 ),用于存储线程私有数据,而 Java 内存模型中规定全部变量都存储在主内存,主内存是共享内存区域,全部线程均可以访问,但线程对变量的操做 ( 读取赋值等 ) 必须在工做内存中进行,首先要将变量从主内存拷贝到本身的工做内存空间,而后对变量进行操做,操做完后再将变量写回主内存,不能直接操做主内存中的变量。架构
咱们知道 Java栈是每一个线程私有的数据区域,别的线程没法访问到不一样线程的私有数据,因此线程须要通讯的话,就必须经过主内存来完成,Java 内存模型就是夹在这二者之间的一组规范,咱们先来看看这个抽象架构图:
从结构图来看,若是线程 A 与线程 B 之间须要通讯的话,必需要经历下面 2 个步骤:
咱们来看一个具体的例子来加深一下理解,看下面这张图:
如今线程 A 须要和线程 B 通讯,咱们已经知道线程之间通讯的两部曲了,假设初始时,这三个内存中的 x 值都为 0。线程 A 在执行时,把更新后的 x 值(假设值为 1)临时存放在本身的本地内存 A 中。当线程 A 和线程 B 须要通讯时,线程 A 首先会把本身本地内存中修改后的 x 值刷新到主内存中,此时主内存中的 x 值变为了 1。随后,线程 B 到主内存中去读取线程 A 更新后的 x 值,此时线程 B 的本地内存的 x 值也变为了 1,这样就完成了一次通讯。
JMM 经过控制主内存与每一个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证。Java 内存模型除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。这套实现也就是咱们经常使用的volatile
、synchronized
、final
等。
Happens-Before 内存模型或许叫作 Happens-Before 原则更为合适,在 《JSR 133 :Java内存模型与线程规范》中,Happens-Before 内存模型被定义成 Java 内存模型近似模型,Happens-Before 原则要说明的是关于可见性的一组偏序关系。
为了方便程序员开发,将底层的烦琐细节屏蔽掉,Java 内存模型 定义了 Happens-Before 原则。只要咱们理解了Happens-Before 原则,无需了解 JVM 底层的内存操做,就能够解决在并发编程中遇到的变量可见性问题。JVM 定义的 Happens-Before 原则是一组偏序关系:对于两个操做A和B,这两个操做能够在不一样的线程中执行。若是A Happens-Before B,那么能够保证,当A操做执行完后,A操做的执行结果对B操做是可见的。
Happens-Before 原则一共包括 8 条,下面咱们一块儿简单的学习一下这 8 条规则。
这条规则是指在一个线程中,按照程序顺序,前面的操做 Happens-Before 于后续的任意操做。这一条规则仍是很是好理解的,看下面这一段代码
class Test{
1 int x ;
2 int y ;
3 public void run(){
4 y = 20;
5 x = 12;
}
}
复制代码
第四行代码要 Happens-Before 于第五行代码,也就是按照代码的顺序来。
这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。例以下面的代码,在进入同步块以前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮咱们实现的
synchronized (this) {
// 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
复制代码
对于锁定规则能够这样理解:假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,可以看到线程 A 对 x 的写操做,也就是线程 B 可以看到 x==12。
这条规则是指对一个 volatile 变量的写操做及这个写操做以前的全部操做 Happens-Before 对这个变量的读操做及这个读操做以后的全部操做。
这条规则是指主线程 A 启动子线程 B 后,子线程 B 可以看到主线程在启动子线程 B 前的操做。
public class Demo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(count);
});
count = 12;
t1.start();
}
}
复制代码
子线程 t1 可以看见主线程对 count 变量的修改,因此在线程中打印出来的是 12 。这也就是线程启动规则
这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 经过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程可以看到子线程的操做。固然所谓的“看到”,指的是对共享变量的操做。
public class Demo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// t1 线程修改了变量
count = 12;
});
t1.start();
t1.join();
// mian 线程能够看到 t1 线程改修后的变量
System.out.println(count);
}
}
复制代码
一个线程在另外一个线程上调用 interrupt ,Happens-Before 被中断线程检测到 interrupt 被调用。
public class Demo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// t1 线程能够看到被中断前的数据
System.out.println(count);
});
t1.start();
count = 25;
// t1 线程被中断
t1.interrupt();
}
}
复制代码
mian 线程中调用了 t1 线程的 interrupt() 方法,mian 对 count 的修改对 t1 线程是可见的。
一个对象的构造函数执行结束Happens-Before它的finalize()方法的开始。“结束”和“开始”代表在时间上,一个对象的构造函数必须在它的finalize()方法调用时执行完。根据这条原则,能够确保在对象的finalize方法执行时,该对象的全部field字段值都是可见的。
这条规则是指若是 A Happens-Before B,且 B Happens-Before C,那么 A Happens- Before C。
目前互联网上不少大佬都有 Java 内存模型系列教程,若有雷同,请多多包涵了。原创不易,码字不易,还但愿你们多多支持。若文中有所错误之处,还望提出,谢谢,欢迎扫码关注微信公众号:「平头哥的技术博文」,和平头哥一块儿学习,一块儿进步。