硬件内存模型到 Java 内存模型,这些硬核知识你知多少?
Java 内存模型跟上一篇 JVM 内存结构很像,我经常会把他们搞混,但其实它们不是一回事,而且相差还很大的,希望你没它们搞混,特别是在面试的时候,搞混了的话就会答非所问,影响你的面试成绩,当然也许你碰到了半吊子面试官,那就要恭喜你了。Java 内存模型比 JVM 内存结构复杂很多,Java 内存模型有一个规范叫:《JSR 133 :Java内存模型与线程规范》,里面的内容很丰富,如果你没看过的话,我建议你看一下。今天我们就简单的来聊一聊 Java 内存模型,关于 Java 内存模型,我们还是先从硬件内存模型入手。
硬件内存模型
先来看看硬件内存简单架构,如下图所示:
这是一幅简单的硬件内存结构图,真实的结构图要比这复杂很多,特别是在缓存层,现在的计算机中 CPU 缓存一般有三层,你也可以打开你的电脑看看,打开 任务资源管理器 ---> 性能 ---> cpu ,如下图所示:
从图中可以看出我这台机器的 CPU 有三级缓存,一级缓存 (L1) 、二级缓存(L2)、三级缓存(L3),一级缓存是最接近 CPU 的,三级缓存是最接近内存的,每一级缓存的数据都是下一级缓存的一部分。三级缓存架构如下图所示:
现在我们对硬件内存架构有了一定的了解,我们来弄明白一个问题,为什么需要在 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 内存模型可以理解为遵照多核硬件架构的设计,用 Java 实现了一套 JVM 层面的“缓存一致性”,这样就可以规避 CPU 硬件厂商的标准不一样带来的风险。好了,正式介绍一下 Java 内存模型:Java 内存模型 ( Java Memory Model,简称 JMM ),本身是种抽象的概念,并不是像硬件架构一样真实存在的,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量 (包括实例字段、静态字段和构成数组对象的元素) 的访问方式,更多关于 Java 内存模型知识可以阅读 JSR 133 :Java内存模型与线程规范。
我们知道 JVM 运行程序的实体是线程,在上一篇 JVM 内存结构中我们得知每个线程创建时,JVM 都会为其创建一个工作内存 ( Java 栈 ),用于存储线程私有数据,而 Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作 ( 读取赋值等 ) 必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完后再将变量写回主内存,不能直接操作主内存中的变量。
我们知道 Java栈是每个线程私有的数据区域,别的线程无法访问到不同线程的私有数据,所以线程需要通信的话,就必须通过主内存来完成,Java 内存模型就是夹在这两者之间的一组规范,我们先来看看这个抽象架构图:
从结构图来看,如果线程 A 与线程 B 之间需要通信的话,必须要经历下面 2 个步骤:
- 首先,线程 A 把本地内存 A 中的共享变量副本中的值刷新到主内存中去。
- 然后,线程 B 到主内存中去读取线程 A 更新之后的值,这样线程 A 中的变量值就到了线程 B 中。
我们来看一个具体的例子来加深一下理解,看下面这张图:
现在线程 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 内存模型或许叫做 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 条规则。
1、程序顺序规则
这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。这一条规则还是非常好理解的,看下面这一段代码
class Test{
1 int x ;
2 int y ;
3 public void run(){
4 y = 20;
5 x = 12;
}
}
第四行代码要 Happens-Before 于第五行代码,也就是按照代码的顺序来。
2、锁定规则
这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的
synchronized (this) {
// 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
对于锁定规则可以这样理解:假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。
3、volatile变量规则
这条规则是指对一个 volatile 变量的写操作及这个写操作之前的所有操作 Happens-Before 对这个变量的读操作及这个读操作之后的所有操作。
4、线程启动规则
这条规则是指主线程 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 。这也就是线程启动规则
5、线程结束规则
这条是关于线程等待的。它是指主线程 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);
}
}
6、中断规则
一个线程在另一个线程上调用 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 线程是可见的。
7、终结器规则
一个对象的构造函数执行结束Happens-Before它的finalize()方法的开始。“结束”和“开始”表明在时间上,一个对象的构造函数必须在它的finalize()方法调用时执行完。根据这条原则,可以确保在对象的finalize方法执行时,该对象的所有field字段值都是可见的。
8、传递性规则
这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens- Before C。
最后
目前互联网上很多大佬都有 Java 内存模型系列教程,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢,欢迎扫码关注微信公众号:「平头哥的技术博文」,和平头哥一起学习,一起进步。
硬件内存模型到 Java 内存模型,这些硬核知识你知多少?的更多相关文章
- JVM内存结构 VS Java内存模型 VS Java对象模型
前面几篇文章中, 系统的学习了下JVM内存结构.Java内存模型.Java对象模型, 但是发现自己还是对这三者的概念和区别比较模糊, 傻傻分不清楚.所以就有了这篇文章, 本文主要是对这三个技术点再做一 ...
- Java内存区域与Java内存模型
Java内存区域 Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有其用途以及创建销毁的时机,其中蓝色部分代表的是所有线程共享的数据区域,而绿色部分代表的是每个线程的私有 ...
- 【JVM】JVM内存结构 VS Java内存模型 VS Java对象模型
原文:JVM内存结构 VS Java内存模型 VS Java对象模型 Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清 ...
- 【转】JVM内存结构 VS Java内存模型 VS Java对象模型
JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途. 其中有些区域随着虚拟机进程的启动而 ...
- 区分 JVM 内存结构、 Java 内存模型 以及 Java 对象模型 三个概念
本文由 简悦 SimpRead 转码, 原文地址 https://www.toutiao.com/i6732361325244056072/ 作者:Hollis 来源:公众号Hollis Java 作 ...
- [转帖]JVM内存结构 VS Java内存模型 VS Java对象模型
JVM内存结构 VS Java内存模型 VS Java对象模型 https://www.hollischuang.com/archives/2509 Java作为一种面向对象的,跨平台语言,其对象.内 ...
- Java内存管理:Java内存区域 JVM运行时数据区
转自:https://blog.csdn.net/tjiyu/article/details/53915869 下面我们详细了解Java内存区域:先说明JVM规范定义的JVM运行时分配的数据区有哪些, ...
- JVM内存结构、Java内存模型和Java对象模型
Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚.比如本文要讨论的JVM内存结构.Java内存模型和Java对象模型 ...
- JVM自动内存管理机制——Java内存区域(下)
一.虚拟机参数配置 在上一篇<Java自动内存管理机制——Java内存区域(上)>中介绍了有关的基础知识,这一篇主要是通过一些示例来了解有关虚拟机参数的配置. 1.Java堆参数设置 a) ...
随机推荐
- 【Java】web实现图片在线预览
一.场景还原 用户上传了一张图片,已有服务器保存路径,现由于系统配置无法直接通过图片URL打开预览图片,需实现点击预览将图片显示在浏览器上. 二.实现方法 html: <a href=" ...
- Python接口测试框架实战与自动化进阶☝☝☝
Python接口测试框架实战与自动化进阶☝☝☝ 一.fiddler在工作中的运用 1.如何抓接口 抓紧手机端接口 ①.在电脑终端输入:ipconfig ,找到电脑ip ②.打开手机,连接WiFi, ...
- 《java编程思想》P160-P180(第八章部分+第九章部分)
1.什么是多态? 多态的定义:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同的行为方式.(发送消息就是函数调用) 现实中,关于多态的例子不胜枚举.比方说按下 F1 ...
- python 中的一点新知识
逻辑行与物理行 所谓物理行(Physical Line)是你在编写程序时 你所看到 的内容.所谓逻辑行(Logical Line)是 Python 所看到 的单个语句.Python 会假定每一 物理行 ...
- Python_函数传参
关于函数中传递参数的相关知识 其中 万能参数 第一次听说 但感觉用处不大 后面用到再详细整理
- SVN工具常用功能总结
使用SVN作为版本管理工具,可以使用VisualSVN Server+TortoiseSVN搭建SVN版本控制系统,组长安装VisualSVN Server,组员安装TortoiseSVN. Tort ...
- Nginx基本属性配置详解
1. Nginx服务的基本配置 1.1 用于调试进程和定位问题的配置项 是否以守护进程的方式运行nginx # 默认on daemon on|off; 是否以master/worker方式工作 # 默 ...
- Spring容器启动源码解析
1. 前言 最近搭建的工程都是基于SpringBoot,简化配置的感觉真爽.但有个以前的项目还是用SpringMvc写的,看到满满的配置xml文件,却有一种想去深入了解的冲动.折腾了好几天,决心去写这 ...
- Pycharm中Python Console与Terminal的区别
1.Python Console是Python交互式模式,可以直接输入代码,然后执行,并立刻得到结果 2.Terminal是命令行模式,与系统的CMD(命令提示符)一样,可以运行各种系统命令
- kubernetes kubelet组件中cgroup的层层"戒备"
cgroup是linux内核中用于实现资源使用限制和统计的模块,docker的风靡一时少不了cgroup等特性的支持.kubernetes作为容器编排引擎,除了借助docker进行容器进程的资源管理外 ...