聊聊Java内存模型
一、Java内存模型
硬件处理
电脑硬件,我们知道有用于计算的cpu、辅助运算的内存、以及硬盘还有进行数据传输的数据总线。在程序执行中很多都是内存计算,cpu为了更快的进行计算会有高速缓存,最后同步至主内存,大概的交互如下图

为了使处理器内部的运算单元能够被充分的利用,处理器可能会对输入代码进行乱序执行优化,然后将计算后的结果进行重组,保证该结果和顺序执行的结果是一致的(单位时间内,一个core只能执行一个线程,所以结果的一致仅限一个线程内)。
Java内存模型
Java内存模型是语言级别的模型,它的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取数变量这样的底层细节。
在内存里,java内存模型规定了所有的变量都存储在主内存(物理内存)中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行。不同的线程无法访问别线程的工作内存里的内容。下图展示了逻辑上 线程、主内存、工作内存的三者交互关系。

内存交互操作
Java内存模型定义的8个操作指令来进行内存之间的交互
read 读取主内存的值,并传输至工作内存
load 将read的变量值存放到工作内存
read
好比快递运输车,工作内存好比站点,运输车将快递运输到站点,站点必须得卸货 load
。
use 将工作内存的变量值,传递给执行引擎
assign 执行引擎对变量进行赋值
use
好比站点进行快递员分配,站点说我把快递分给你了快递员A。快递员A接收到快递assign
开始派送。
store 工作内存将变量传输到主内存
write 主内存将工作内存传递过来的变量进行存储
store
和 write
就好理解了,快递员A将快递送到你家门口(store),然后你得签收(write)
lock 用作主内存变量,它把一个变量在内存里标识为一个线程独占状态
unlock 用作主内存变量,它对被锁定的变量进行解锁
下图展示下工作内存和主内存间的指令操作交互

指令规则
- read 和 load、store和write必须成对出现
- assign操作,工作内存变量改变后必须刷回主内存
- 同一时间只能运行一个线程对变量进行lock,当前线程lock可重入,unlock次数必须等于lock的次数,该变量才能解锁
- 对一个变量lock后,会清空该线程工作内存变量的值,重新执行load或者assign操作初始化工作内存中变量的值。
- unlock前,必须将变量同步到主内存(store/write操作)
二、重排序
从java源码到最终实际执行的指令序列,会经历下面3种重排序

重排序的现象
a=1,b=a 这一组 b依赖a,不会重排序;
a=1,b=2 这一组 a和b没有关系,那么就有可能被重排序执行 b=2,a=1
编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句执行顺序指令级并行的重排序
现代处理器采用了指令级并行技术将多条指令重叠执行。在不存在数据依赖的时候,处理器可以改变指令执行顺序内存系统重排序
处理器使用高速缓存,使得多运算单元加载和存储主内存操作看上去可能在乱序执行
重排序的代码示例,文章底部的参考文章里有示例,这里就不罗列了。
三、内存屏障
JMM(java 内存模型) 在不改变程序执行结果的前提下,尽可能的支持处理器的重排序。通过禁止特定特定类型的编译器重排序和处理器重排序,为开发者提供一致的内存可见性保证,如 volatile
、final
。
Java编译器在生成指令的时候会在适当位置插入内存屏障来进制特定类型的处理器排序。
内存屏障说的通俗一点就是一个栏杆,在两个指令之间插入栏杆,后面的指令就不能越过栏杆先执行。
JMM定义的内存屏障指令分为4类
LoadLoad
指令示例 Load1LoadLoad
Load2
确保Load1数据装载一定先于Load2及后续所有Load指令LoadStore
指令示例 Load1LoadStore
Store2
确保Load1数据装载一定先于Store2及后续所有Store指令StoreStore
指令示例 Store1StoreStore
Store2
确保Store1主内存落地(从工作内存刷入主存,其它线程可见)一定先于Store2及后续所有Store指令StoreLoad
指令示例 Store1StoreLoad
Load2
确保Store1主内存落地(从工作内存刷入主存,其它线程可见)一定先于Load2及后续所有Load指令
处理器对重排序的支持

从上面可以看到不同的处理器架构对重排序的支持也是不一样(其它处理器架构暂不罗列),所以不同的平台JMM的内存屏障施加也略有不同,具体来说,比如 X86 对Load1Load2不支持重排序,那么你就没有必要施加 LoadLoad
屏障。
四、volatile的内存语义
volatile我们都知道是java的关键字用来保证数据可见性,防止指令重排的效果。包括JUC里AQS Lock的底层实现也是基于volatitle来实现。
volatile写的内存语义
当写一个volatile变量的时候,JMM会把该线程对应的本地内存变量值刷新到主内存
volatile读的内存语义
当读一个volatile变量的时候,JMM会把线程本次内存置为无效。线程接下来将从主内存中读取共享变量(也就是重新从主内存获取值,更新运行内存中的本地变量)
上面两个语义,保证了volatile变量写入对线程的可见性
volatile内存屏障插入规则

代码简单示例
class X {
int a, b;
volatile int v, u; void f() {
int i, j; i = a;// load a 普通load
j = b;// load b 普通load
i = v;// load v volatile load
// LoadLoad
j = u;// load u volatile load
// LoadStore
a = i;// store a 普通store
b = j;// store b 普通store
// StoreStore
v = i;// store v volatile store
// StoreStore
u = j;// store u volatile store
// StoreLoad
i = u;// load u volatile load
// 两个屏障 LoadLoad 和 LoadStore
j = b;// load b 普通load
a = i;// store a 普通store
}
}
上述代码可以套用volatile屏障规则对应。
当然不同的处理器架构重排序的支持也是不一样,比如X86 只有当 store1 load2 的时候会进行重排序,那么就会省略掉很多类型的内存屏障。
五、final的内存语义
final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。
被final修饰的变量不能被修改,方法不能被重写,类不能被继承。
我们暂时把final修饰的称作域,对于final域,编译器和处理器要遵守两个重排序规则
写规则
- 在构造函数内对一个final域的写入,与随后把这个被构造的对象的引用赋值给一个引用变量,这两个操作不可重排序
JMM禁止编译器把final域的写重排序到构造函数之外
编译器会在final域写入的后面插入StoreStore
屏障,禁止处理器把final域的写重排序到构造函数之外。
该规则可以保证在对象引用为任意线程可见之前,对象的final域已经被正确初始化,而普通域无法保障。
读规则
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。
该规则保证在读一个对象的final域之前,一定会先读包含这个域的对象引用。
参考资料
- 《Java并发编程的艺术》
- 《深入理解Java虚拟机》
- http://ifeve.com/jmm-cookbook-mb/
- http://www.cnblogs.com/chenyangyao/p/5269622.html
- http://www.importnew.com/7553.html
聊聊Java内存模型的更多相关文章
- Java并发(1)- 聊聊Java内存模型
引言 在计算机系统的发展过程中,由于CPU的运算速度和计算机存储速度之间巨大的差距.为了解决CPU的运算速度和计算机存储速度之间巨大的差距,设计人员在CPU和计算机存储之间加入了高速缓存来做为他们之间 ...
- 面试官:为什么需要Java内存模型?
面试官:今天想跟你聊聊Java内存模型,这块你了解过吗? 候选者:嗯,我简单说下我的理解吧.那我就从为什么要有Java内存模型开始讲起吧 面试官:开始你的表演吧. 候选者:那我先说下背景吧 候选者:1 ...
- 聊聊高并发(三十四)Java内存模型那些事(二)理解CPU快速缓存的工作原理
在上一篇聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了Java内存模型是一个语言级别的内存模型抽象.它屏蔽了底层硬件实现内存一致性需求的差异,提供了对上层的 ...
- 3.java内存模型以及happens-before规则
1. JMM的介绍 在上一篇文章中总结了线程的状态转换和一些基本操作,对多线程已经有一点基本的认识了,如果多线程编程只有这么简单,那我们就不必费劲周折的去学习它了.在多线程中稍微不注意就会出现线程安全 ...
- Java内存模型(Java Memory Model,JMM)
今天简单聊聊什么叫做 Java 内存模型,不是 JVM 内存结构哦. JMM 是一个语言级别的内存模型,处理器的硬件模型是硬件级别,Java中的内存模型是内存可见性的基本保证.从而为我们 volati ...
- Java内存模型以及happens-before规则
本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...
- Java内存模型之总结
经过四篇博客阐述,我相信各位对Java内存模型有了最基本认识了,下面LZ就做一个比较简单的总结. 总结 JMM规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序.一方面,要为 ...
- 一夜搞懂 | Java 内存模型与线程
前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习内存模型与线程? 并发处理的广泛应用是 Amdah1 定律代替摩尔定律成为计 ...
- 深入浅出Java内存模型
面试官:我记得上一次已经问过了为什么要有Java内存模型 面试官:我记得你的最终答案是:Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台 ...
随机推荐
- WPF概述(硬件加速及分辨率无关性)
一.名词解释 WPF(Windows Presentation Foundation),直译为Windows表示基础,是专门用来编写程序表示层的技术和工具. 大部分程序都是多层架构的,一般至少包含三层 ...
- VS2015ASP.NET MVC5项目中Spring.NET配置方法(超详细)
首先,在ASP.NET MVC5项目右键,如下图所示,选择“管理Nuget程序包...” 然后,在弹出的页面的搜索框中输入“spring.web”,在返回结果中选择Spring.Web和Spring. ...
- Regular Expression
It's a very elegant summary of regular expression from The AWK Programming Language. 1. The regular ...
- 【UOJ244】 【UER #7】短路(贪心)
传送门 uoj Solution 简单题? 考虑一条路径长什么样子,一定是经过某一个字母环的左上角,那么答案就很简单了. 我们记一个前缀最小值,这样子让他一路走下去一定是最优! 然后扫一遍就好了. 代 ...
- NVIDIA GRID 和 NICE DCV 技术用于实现 Linux 和 Windows® 图形加速虚拟桌面
NVIDIA GRID 和 NICE DCV 技术用于实现 Linux 和 Windows® 图形加速虚拟桌面. NICE DCV: 满足 LINUX 和 WINDOWS 的远程 3D 通过 NICE ...
- vue教程1-05 事件 简写、事件对象、冒泡、默认行为、键盘事件
vue教程1-05 事件 简写.事件对象.冒泡.默认行为.键盘事件 v-on:click/mouseover...... 简写的: @click="" 推荐 事件对象: @clic ...
- JVM中OutOFMemory和StackOverflowError异常代码
1.Out of Memory 异常 右键Run As --->Run Configuration 设置JVM参数 -Xms20m -Xmx20m 上代码: /** * VM Args:-Xms ...
- python多线程-Semaphore(信号对象)
Semaphore(value=1) Semaphore对象内部管理一个计数器,该计数器由每个acquire()调用递减,并由每个release()调用递增.计数器永远不会低于零,当acquire() ...
- (转)X-Frame-Options响应头缺失漏洞
原文:https://blog.csdn.net/ljl890705/article/details/78071601 x-frame-options响应头缺失漏洞. 故名思意,就是返回的响应头信息中 ...
- python实战问题记录
开发环境搭建 1.安装aiohttp这个异步的http框架失败 提示使用pip更高版本,但是更新之后,还是无法使用.所以,我们采用anaconda中的aiohttp,即打开anaconda,然后进入E ...