写点什么

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:171173

评论

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

聊聊 Kafka:Kafka 消息丢失的场景以及最佳实践

老周聊架构

kafka 4月月更 5月月更

Nginx 如何将所有 HTTP 的流量转移到 HTTPS

HoneyMoose

C语言_Linux基本命令与C语言基础

DS小龙哥

5月月更

2个不同的对象集合如何取交集和差集

爱好编程进阶

Java 程序员 后端开发

320000字2021春招高频面试真题汇总

爱好编程进阶

Java 程序员 后端开发

Flutter/Dart:生成最小值和最大值之间的随机数

坚果

5月月更

【Go实现】实践GoF的23种设计模式:建造者模式

元闰子

Go 设计模式 建造者模式

网站开发进阶(二十六)JavaScript 实现页面刷新方法汇总

No Silver Bullet

JavaScript 页面刷新 5月月更

15-拦截器

爱好编程进阶

Java 程序员 后端开发

企评家,打造专业的企业大数据SaaS平台

企评家

企业大数据 企评家 企业成长性评价

python进阶-迭代器和生成器

AIWeker

Python 人工智能 5月月更

IntelliJ IDEA 如何增加运行时候的内存

HoneyMoose

SpringSecurity认证流程分析

急需上岸的小谢

5月月更

2021 年最新版 68道Redis面试题,20000字,赶紧收藏起来备用

爱好编程进阶

Java 程序员 后端开发

时序数据库在水电站领域的应用

CnosDB

IoT 时序数据库 开源社区 CnosDB infra

Nacos源码系列—关于服务注册的那些事

牧小农

源码 nacos

从零构建物联网平台-给个理由先

老任物联网杂谈

物联网平台

MongoDB 入门教程系列之二:使用 Spring Boot 操作 MongoDB

汪子熙

node.js 数据库 mongodb 分布式数据库 5月月更

闲置计费 | Serverless 冷启动与成本间的最优解

阿里巴巴云原生

阿里云 Serverless 云原生 函数计算

MongoDB 入门教程系列之三:使用 Restful API 操作 MongoDB

汪子熙

数据库 mongodb 分布式数据库 分布式数据库mongodb 5月月更

聊聊 C 语言和 ABAP 这两门编程语言的关系

汪子熙

编程语言 C语言 SAP abap 5月月更

用户行为分析模型实践(二)—— 漏斗分析模型

vivo互联网技术

大数据 数据分析 Clickhouse

无需修改代码,用 fcapp.run 运行你的 REST 应用

阿里巴巴云原生

阿里云 Serverless 云原生 函数计算

CleanMyMac2022免费版Mac电脑清理软件功能

茶色酒

CleanMyMac2022 CleanMyMac

MySQL存储过程批量生成假用户电话号码

芝士味的椒盐

MySQL MySQL 数据库 5月月更

Docker下的Spring Cloud三部曲之一:极速体验

程序员欣宸

Java Spring Cloud 5月月更

《对线面试官》Java注解

Java3y

Java 程序员 面试 编程语言 5月月更

10-2 5-2 查询至少生产两种不同的计算机(PC或便携式电脑)且机器速度至少为133的厂商 (20 分)(思路加详解+测试用例

爱好编程进阶

程序员 后端开发

Django Model 如何返回空的 QuerySet

AlwaysBeta

django

虎符交易所上线量化网格交易 同步开启活动三重奏

区块链前沿News

活动 虎符交易所

2021-6-1【利用指针方法求数组的最大值和最小值】

爱好编程进阶

Java 程序员 后端开发

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