写点什么

Java 内存模型

  • 2019-09-24
  • 本文字数:5253 字

    阅读完需:约 17 分钟

Java内存模型

1 物理内存模型

现代计算机的物理内存模型:



现代计算机的物理内存模型


现在计算机最少的都是应该是双核,当然我们也经常在买个人电脑的时候听过四核四线程、四核八线程等,可以说现在个人电脑标配都是四核心了,为了方便上图只是列举了 2 个核心。现代计算机的内存在逻辑上还是一块。


有人可能问:不对啊,我电脑就插了两块内存,但是操作系统会把两块内存的地址统一抽象,比如每一块的内存是 2048MB 地址是 000000000000-011111111111MB,两块就是 0000000000000-0111111111111MB,操作系统会统一编址。所以整体上看还是一块内存。因为 CPU 的操作速度太快,如果让 CPU 直接操作内存,那么就是对 CPU 资源的一种巨大浪费,为了解决这个问题现在计算机都给 CPU 加上缓存,比如一级缓存,二级缓存,甚至三级缓存。缓存速度比内存快,但是是还是赶不上 CPU 的数据级别,所以在缓存和 CPU 之间又有了 register,register 的存储速度比缓存就快了好多了。


存储速度上有如下关系:


register > 一级缓存 > 二级缓存 > … > n 级缓存 > 内存


容量上一般有如下关系:


内存 > n 级缓存 > … > 二级缓存 > 一级缓存 > register


之所以可以用缓存和 register 来缓解 CPU 和内存之间巨大的速度差别是基于如下原理:


CPU 访问过的内存地址,很有可能在短时间内会被再次访问。


所以,比如 CPU 访问了地址为 0x001fffff 的内存地址,如果没有缓存和 register,那么 CPU 再下次访问这个内存地址的时候就还要去内存读,但是如果有缓存,缓存会把 CPU 访问过的数据先存储起来,等 CPU 待会再找地址为 0x001fffff 的内存地址时候,发现其在缓存中就存在了,那么好了,这就不用在访问内存了。速度自然就提升了。这就涉及到计算机组成原理的知识了,如果想了解可以 google 一下,这里就不在做更深的介绍了到这里就够用了。

2 并发中三个重要概念

了解现代计算机物理内存模型工作原理后,那么再理解多线程开发中最关心的三个概念就有的放矢了。先介绍下三个概念:

2.1 操作原子性 :

一个操作要么全做,要么全不做,那么这个操作就符合原子性。比如你给你老婆银行卡转 500 块钱,就包括两个操作,自己账户先减 500,你老婆账户加 500。这个转账操作应该满足原子性。如果银行只执行了你自己账户的扣钱操作,没有执行给你老婆账户的加钱操作。丢了 500 块钱是小事,被老婆大人罚跪搓衣板可就不得了了。所以你自己账户减钱,老婆账户加钱,这两个操作要么都做了,要么都别做。例如如下操作:


a = a + 1;


结合我们上述的现代计算机的内存模型,计算机执行 a=a+1 时候会分成三个原子性操作:


1)把 a 的值(比如 4)从内存中取出放到 CPU 的缓存系统中


2)从缓存系统中取出 a 的值加 1(4+1)得到新结果


3)把新结果存回到内存中


一个“a=a+1”操作计算机中被拆分成三个原子性操作,那么完全可以出现 CPU 执行完 1.操作后,去执行别的操作了。这就是并发操作原子性问题的根本来源。

2.2 操作有序性 :

例如如下代码:


 1public class A { 2public int a; 3public boolean b = false; 4 5public void methodA(){ 6    a = 3; 7    b = true; 8    a = a + 1; 9}1011public void methodB(){12    a = 3;13    b = (a == 4);14    a = a + 1;15}16} 
复制代码


methodA 方法代码先经过 java 编译器编译成字节码,然后字节码然后被操作系统解释成机器指令,在这个解释过程中,操作系统可能发现,咦?在给变量 b 赋值为 true 后又操作了 a 变量,干脆我操作系统自己改改执行顺序,把对 a 变量的两个操作都执行完,然后再执行对 b 的操作,这就叫指令重排序。这样就会节省操作时间,如下图没有进行指令重排序时:



没有指令重排序


图中 CPU 和缓存系统要进行 9 次通信,缓存系统和内存要通信 7 次,假设 cpu 和缓存系统通信一次用时 1ms,缓存系统和内存通信一次用时 10ms,那么总用时 9 乘 1 + 7 乘 10 = 79ms。经过指令重排序后,总共用时 6 乘 1 + 6 乘 10 = 66ms ,如下图所示:



有指令重排序


经过指令重排序的确可以提程序运行效率,所以现代计算机都会对指令进行重排序,但是这种重排序也不是无脑重排序,重排序的基础是前后语句不存在依赖关系时,才有可能发生指令重排序。所以 A 类的 methodB 方法不会发生指令重排序。指令重排序在单线程环境里面这不会有什么问题,但是多线程中就可能发生意外。比如线程 1 中执行如下代码:


1instance.methodA();
复制代码


另一个线程 2 执行如下代码:


1while(instance.a != 4){ //a只要不等4,线程就让出CPU,等待调度器再次执行此线程2 Thread.yield(); //让出CPU,线程进入就绪态3}4System.out.print(instance.b);
复制代码


其中 instance 是 A 类的一个实例。如果线程 1 发生了指令重排序, 那么这线程 2 的打印结果很有可能是 false,这就和我们对代码的直观观察结果出处很大。如果线上产品出错的原因是指令重排序导致的,几乎不能可能排查出来。

2.3 操作可见性 :

在“操作有序性” 中的线程线程 2 ,还有可能会没有任何输出结果。因为线程 2 要想有输出必须要满足 instance.a =4,但这是在线程 1 中调用 methodA 方法后 instance.a 的值才为 4 。而要想让线程 2 看到这个新值,必须要把线程 1 的修改及时写回内存, 同时通知线程 2 存在缓存系统中的 instance.a 值已经过期,需要去内存中获取最新值。如果我们的类 A 和线程 1、线程 2 调用的代码没有特殊的声明,那么操作系统不能保证上述过程一定发生。即可能发生线程 1 对 instance.a 的修改对线程 2 不一定可见,这就是操作的可见性问题。


java 多线程的所有问题都植根于“操作原子性”、“操作有序性”、“操作可见性”而引发的。

3java 内存模型

上面介绍了现代计算机的内存模型以及其引起的在并发编程的三个问题,下面来介绍下 java 的内存模型。java 为了实现其夸平台的特性,使用了一种虚拟机技术,java 程序运行在这虚拟机上,那么不管你是 windows 系统,linux 系统,unix 系统,只要我 java 虚拟机屏蔽一切操作系统带来的差异,向 java 程序提供专用的、各系统无差别的虚拟机,那么 java 程序员就不需要关心底层到底是什么操作系统了。对于 int 类型的变量其取值范围永远是 -2^31 -1 至 2^31,即 4 个字节。但是对 C\C++,这个操作系统的 int 可能是 4 字节,那个可能是 8 字节。C++程序员跨平台写代码,痛苦异常。这个给我们编程带来极大方便的虚拟机就是大名鼎鼎的 JVM(Java Virtual Machine)。既然是虚拟机那么就需要模拟真正物理机的所有设备,像 CPU,网络,存储等。和我们程序员最密切的就是 JVM 的存储,这就是 java 内存模型(Java Memory Model 简称 JMM)。有别于我们真实的物理存储模型,JMM 把存储分为线程栈区和堆区。在 JVM 中的每个线程都有自己独立的线程栈,而堆区用来存储 java 的对象实例。


java 中各种变量的存储有一下规则:


1)成员变量一定存储在堆区。


2)局部变量如果是基本数据类型存储在线程栈中,如果是非基本数据类型存储,其引用存储在线程栈中,但具体的对象实例还是存储在栈中。


因为 java 内存模型是在具体的物理内存模型的基础上实现的,并且为了运行效率,java 也支持指令重排序。所以 java 并发编程也有“原子性”、“有序性”、“可见性”三个问题。但是,我们的 JMM 也不是白吃干饭什么也做的,最起码运行在 JVM 上的代码就具备 8 个内存特性,来使得 java 代码有一定的“有序性”和“可见性”。这些特性也被称为 happen-before 原则。


八大 happen-before 特性:


  • 单线程 happen-before 原则:在同一个线程中,书写在前面的操作 happen-before 后面的操作。

  • 锁的 happen-before 原则:同一个锁的 unlock 操作 happen-before 此锁的 lock 操作。

  • volatile 的 happen-before 原则:对一个 volatile 变量的写操作 happen-before 对此变量的任意操作(当然也包括写操作了)。

  • happen-before 的传递性原则:如果 A 操作 happen-before B 操作,B 操作 happen-before C 操作,那么 A 操作 happen-before B 操作。

  • 线程启动的 happen-before 原则:同一个线程的 start 方法 happen-before 此线程的其它方法。

  • 线程中断的 happen-before 原则:对线程 interrupt 方法的调用 happen-before 被中断线程的检测到中断发送的代码。

  • 线程终结的 happen-before 原则:线程中的所有操作都 happen-before 线程的终止检测。

  • 对象创建的 happen-before 原则:一个对象的初始化完成先于他的 finalize 方法调用。


happen-before 在这里不能理解成在什么之前发生,它和时间没有任何关系。个人感觉解释成“生效可见于” 更准确。


下面通过对这八个原则详细解释来加深对“生效可见于”的理解。


在同一个线程中,书写在前面的操作 happen-before 后面的操作:


好多文章把这理解成书写在前面先发生于书写在后面的代码,但是指令重排序,确实可以让书写在后面的代码先于书写在前面的代码发生。这是里把 happen-before 理解成“先于什么发生”,其实 happen-beofre 在这里没有任何时间上的含义。比如下面的代码:


1int a = 3;      //12int b = a + 1; //2
复制代码


这里 //2 对 b 赋值的操作会用到变量 a,那么 java 的“单线程 happen-before 原则”就保证 //2 的中的 a 的值一定是 3,而不是 0,5,等其他乱七八糟的值,因为//1 书写在//2 前面, //1 对变量 a 的赋值操作对//2 一定可见。因为//2 中有用到//1 中的变量 a,再加上 java 内存模型提供了“单线程 happen-before 原则”,所以 java 虚拟机不许可操作系统对//1 //2 操作进行指令重排序,即不可能有//2 在//1 之前发生。但是对于下面的代码:


1 int a = 3;2 int b = 4;
复制代码


两个语句直接没有依赖关系,所以指令重排序可能发生,即对 b 的赋值可能先于对 a 的赋值。


同一个锁的 unlock 操作 happen-beofre 此锁的 lock 操作:


话不多说直接看下面的代码:


 1``` 2public class A { 3public int var; 4 5private static A a = new A(); 6 7private A(){} 8 9public static A getInstance(){10    return a;11}1213public synchronized void method1(){14    var = 3;15}1617public synchronized void method2(){18    int b = var;19}2021public void method3(){22    synchronized(new A()){ //注意这里和method1 method2 用的可不是同一个锁哦23        var = 4;24    }25  }26} 1//线程1执行的代码:2A.getInstance().method1(); 1//线程2执行的代码:2A.getInstance().method2(); 1//线程3执行的代码:2A.getInstance().method3();
复制代码


如果某个时刻执行完“线程 1” 马上执行“线程 2”,因为“线程 1”执行 A 类的 method1 方法后肯定要释放锁,“线程 2”在执行 A 类的 method2 方法前要先拿到锁,符合“锁的 happen-before 原则”,那么在“线程 2”中 method2 方法中的变量 var 一定是 3,所以变量 b 的值也一定是 3。但是如果是“线程 1”、“线程 3”、“线程 2”这个顺序,那么最后“线程 2”method2 方法中的 b 值是 3,还是 4 呢?其结果是可能是 3,也可能是 4。的确“线程 3”在执行完 method3 方法后的确要 unlock,然后“线程 2”有个 lock,但是这两个线程用的不是同一个锁,所以 JMM 这个两个操作之间不符合八大 happen-before 中的任何一条,所以 JMM 不能保证“线程 3”对 var 变量的修改对“线程 2”一定可见,虽然“线程 3”先于“线程 2”发生。


对一个 volatile 变量的写操作 happen-before 对此变量的任意操作:


1volatile int a;1a = 1; //11b = a;  //2
复制代码


如果线程 1 执行//1,“线程 2”执行了//2,并且“线程 1”执行后,“线程 2”再执行,那么符合“volatile 的 happen-before 原则”所以“线程 2”中的 a 值一定是 1。


如果 A 操作 happen-before B 操作,B 操作 happen-before C 操作,那么 A 操作 happen-before C 操作:


如果有如下代码块:


1volatile int var;2int b;3int c;1b = 4; //12var = 3; //21c = var; //32c = b; //4
复制代码


假设“线程 1”执行//1 //2 这段代码,“线程 2”执行//3 //4 这段代码。如果某次的执行顺序如下:


//1 //2 //3 //4。那么有如下推导( hb(a,b)表示 a happen-before b):


因为有 hb(//1,//2) 、hb(//3,//4) (单线程的 happen-before 原则)

且 hb(//2,//3) (volatile 的 happen-before 原则)

所以有 hb(//1,//3),可导出 hb(//1,//4) (happen-before 原则的传递性)

所以变量 c 的值最后为 4


如果某次的执行顺序如下:


//1 //3 //2// //4 那么最后 4 的结果就不能确定。其原因是 //3 //2 的顺序不符合上述八大原则中的任何一个,不能通过传递性推测出来什么。


通过对上面的四个原则的详细解释,省下的四个原则就比较显而易见了。这里就不做详细解释了。

4 结束语

本文核心是通过现代计算机的内存模型引出 java 虚拟机 JMM 所支持的和并发相关的 8 个原则。这 8 个原则是 JMM 原生支持的,如果想深入理解并且运营 java 的并发机制,那么对 8 个原则的了解是必要的,而不是简单的会用 java 并发库的各种类。


作者介绍:


一页书(企业代号名),目前负责贝壳找房 java 后台开发工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/k0cQ_kIs4FhfAz_ncctRBg


2019-09-24 18:171112

评论

发布
暂无评论
发现更多内容

一图看懂 | 2021阿里云混合云的高能时刻

科技

架构训练营 - 模块五作业

伊静西蒙

一文带你快速拆解云智慧前端技术架构

云智慧AIOps社区

前端 前端开发 可视化 框架 技术干货

零基础如何上手APICloud App、小程序多端开发

YonBuilder低代码开发平台

前端开发 APP开发 APICloud 多端开发 小程序开发

网络安全kali渗透学习 web渗透入门 Layer子域名挖掘机收集信息

学神来啦

浅析安全反序列化漏洞

网络安全学海

黑客 网络安全 信息安全 渗透测试 安全漏洞

深入剖析 HDFS 3.x 新特性-纠删码

五分钟学大数据

hdfs 1月月更

阿里云EMAS 12月产品动态更新

移动研发平台EMAS

阿里云 移动研发平台 emas

XSS跨站脚本攻击:获取键盘记录

喀拉峻

深入浅出Apache Pulsar(3):Pulsar Schema

云智慧AIOps社区

云原生 消息中间件 schema Apache Pulsar 社区 java 编程

Hyperf结合Redis异步队列任务async-queue实现后台操作日志写入

Owen Zhang

hyperf async-queue Redis异步队列任务

Apache APISIX 社区双周报 | 1.28 线上直播预约开启

API7.ai 技术团队

后端 社区周报

来自开发者的点赞!网易云信揽获三大技术奖项

网易云信

资讯

科技为驱,创新为翼——鲸鲮科技喜获“2021北京软件核心竞争力企业”评价

鲸鲮JingOS

操作系统 创新 信创 信息化 科技企业

【OpenMLDB Meetup #1】会议纪要

第四范式开发者社区

机器学习 第四范式 OpenMLDB 特征平台

中国AIOps们,你们究竟是在骗谁?

码农一米

云计算 云服务

架构实战营模块五作业

zhongwy

架构实战营

Hive底层 explain 执行计划详解

五分钟学大数据

hive 1月月更

Wi-Fi 6 提升了哪些方面?

BUG侦探

wifi MU-MIMO Wi-Fi 6 协商速率

深入分析H2数据库控制台中无需身份验证的RCE漏洞

H

数据库 网络安全 漏洞

百亿级监控场景大数据分位值计算实践

百度Geek说

大数据 后端

WPS最大的败笔是“免费用,广告弹窗”,难怪用户纷纷使用office

淋雨

Office

详解策略梯度算法

行者AI

人工智能 强化学习

恒源云gpushare.com_Byte-Pair Encoding算法超详细讲解

恒源云

自然语言处理 深度学习 NLP 大模型

投稿有奖丨阿里云云服务器ECS开发实践征文活动

阿里云弹性计算

阿里云 ECS 征文活动

第五周作业

cqyanbo

政法委跨单位重点人员联防联控系统开发,重点人员管理平台

a13823115807

火山引擎MARS-APMPlus专栏——iOS Heimdallr 卡死卡顿监控方案与优化之路

字节跳动终端技术

ios 字节跳动 性能调优 应用性能监控产品 运维监控

.Net Minimal API 介绍

MASA技术团队

C# .net 微软 接口 API

javaagent

淡泊明志、宁静致远

javaagent

有了小程序还要不要做app?

石云升

小程序 1月月更

Java内存模型_文化 & 方法_一页书_InfoQ精选文章