Java内存模型JMM与可见性
Java内存模型JMM与可见性
标签(空格分隔): java
1 何为JMM
JMM:通俗地讲,就是描述Java中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

结合上图,先介绍几个概念:
主内存:保存了所有的变量。
共享变量:如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量。
工作内存:每个线程都有自己的工作内存,线程独享,保存了线程用到了变量的副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互。
JMM对共享内存的操作做出了如下两条规定:
- 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
- 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。
2 共享变量在线程间的可见性
由此可见不同线程都是直接操作自身工作内存中的副本,因此可能导致共享变量的修改在线程间不可见,所谓不可见,是指一个线程对共享变量的修改不能及时地被其他线程看到。导致共享变量在进程间不可见的原因有以下几个:
- 指令重排序 & 线程交叉执行
- 共享变量更新后的值没有在工作内存和主内存间及时更新
线程交叉执行:主要指线程调度。
指令重排序:为了发挥CPU性能,指令执行顺序可能与书写的不同,分为编译器优化的重排序(编译器优化),指令集并行重排序(处理器优化),内存系统的重排序(处理器优化)。
共享变量更新:如果想让线程A对共享变量的修改被线程B看到,需要以下步骤:把线程A的工作内存中更新过的变量刷新到主内存中,再将主内存中最新的共享变量的值刷新到线程B的工作内存中。如果更新不及时,则会导致共享变量的不可见,数据不准确,线程不安全。
说到重排序,就不得不说一下as-if-serial语义:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致。(编译器,运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)
下面看一段代码:
public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
public PossibleReordering() {}
public static void main(String[] args) throws InterruptedException {
int result[] = new int[4];
for(int i = 0; i < 1000000; i++){
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
x=y=a=b=0;
one.start();
two.start();
one.join();
two.join();
//注意,此时线程one和two均以结束,其对x,y的修改已经写回到主内存
int r = (x << 1) | (y);
result[r]++;
}
System.out.println(Arrays.toString(result));
}
}
该段代码可能输出的x和y有4种组合,分别为(0,0) (0,1) (1,0) (1,1)。一次典型的运行输出如下:[4, 941466, 58524, 6]。该输出代表(0,0)组合产生了4次,(0,1)组合产生了941466次,(1,0)组合产生了58524次,(1,1)组合产生了6次。
各种组合及其可能的原因如下表:
| 组合 | 可能的产生原因 |
|---|---|
| 0 1 | 线程one在two开始之前就完成 |
| 1 0 | 线程two在one开始前就完成 |
| 1 1 | 线程one和two交叉执行的结果 |
| 0 0 | 乱序执行或共享变量更新到主内存不及时 |
由此可见,因此在没有正确同步的情况下,即是要推断最简单的并发程序的行为也很困难。
可能的一种乱序执行情况如下图所示:

重排序不会导致单线程的内存可见性问题,但多线程交错执行时,可能导致可见性问题,那么如何解决线程间对共享变量修改的可见性问题呢?
3 Java在语言层面实现可见性的两种方式
使用synchronized实现可见性:
Java中synchronized关键字有两重含义,一是大家所熟知的实现原子性,二就是实现内存可见性。
synchronized可见性规范:
- 线程解锁前必须把共享变量的最新值刷新到主内存中
- 线程加锁时,将清空工作内存中共享变量的值,从而需要从主内存中重新读取最新值。
因此,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。整个过程如下:获得互斥锁-》清空工作内存-》从主内存拷贝-》执行代码-》写回主内存-》释放互斥锁。
与此同时,synchronized还会限制编译器、运行时和硬件对内存操作重排序的方式,从而在实施重排序时不会破坏JMM提供的可见性保证。
| 共享变量在线程间不可见的原因 | synchronized解决方案 |
|---|---|
| 重排序 & 线程交叉执行 | 原子性(结合as-if-serial语义) |
| 共享变量未及时更新 | 通过synchronized可见性规范 |
使用volatile实现可见性:
Java中的volatile可以保证volatile变量的可见性,但不保证复合操作的原子性(如++)
volatile可见性规范:
- 对volatile变量执行写操作时,会在写操作后加入一条store写屏障指令,强制将缓存刷新到主内存中
- 对volatile变量执行读操作时,会在读操作前加入一条load读屏障指令,强制使缓冲区缓存失效,所以会从主内存读取最新值。
- 防止指令重排序。
通俗来讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样在任何时刻,不同的线程总能看到该变量的最新值。
| 共享变量在线程间不可见的原因 | volatile解决方案 |
|---|---|
| 重排序 & 线程交叉执行 | 防止指令重排序 |
| 共享变量未及时更新 | 通过volatile可见性规范 |
synchronized与volatile对比:
- volatile不需要加锁,比synchronized轻量,不会阻塞线程。
- 从内存可见性角度来看,volatile读相当于加锁,volatile写相当于解锁。
- synchronized可以保证可见性+原子性。volatile只能保证可见性,不能保证原子性。
参考资料
慕课网:细说Java多线程之内存可见性
Java并发编程实战
程晓明:深入理解Java内存模型(一)——基础
Java内存模型JMM与可见性的更多相关文章
- Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)
JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...
- 多线程并发之java内存模型JMM
多线程概念的引入是人类又一次有效压寨计算机的体现,而且这也是非常有必要的,因为一般运算过程中涉及到数据的读取,例如从磁盘.其他系统.数据库等,CPU的运算速度与数据读取速度有一个严重的不平衡,期间如果 ...
- 全面理解Java内存模型(JMM)及volatile关键字(转载)
关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...
- 全面理解Java内存模型(JMM)及volatile关键字(转)
原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...
- 对多线程java内存模型JMM
多线程概念的引入体现了人类重新有效压力寨计算机.这是非常有必要的,由于所涉及的读数据的过程中的一般操作,如从磁盘.其他系统.数据库等,CPU计算速度和数据读取速度已经严重失衡.假设印刷过程中一个线程将 ...
- 深入理解Java内存模型JMM与volatile关键字
深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...
- Java并发编程:Java内存模型JMM
简介 Java内存模型英文叫做(Java Memory Model),简称为JMM.Java虚拟机规范试图定义一种Java内存模型来屏蔽掉各种硬件和系统的内存访问差异,实现平台无关性. CPU和缓存一 ...
- 深入理解Java内存模型JMM
本文转载自深入理解Java内存模型JMM JMM基础与happens-before 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执 ...
- 什么是Java内存模型(JMM)
什么是java内存模型 缓存一致性问题 在现代计算机中,因为CPU的运算速度远大于内存的读写速度,因此为了不让CPU在计算的时候因为实时读取内存数据而影响运算速度,CPU会加入一层缓存,在运算之前缓存 ...
随机推荐
- Kooboo 加Search功能 必须先ReBuild Index Data
加Search功能 有几个要点 1. 需要在Kooboo 必须先 ReBuild Index Data 2. 需要在要搜索的page中启用搜索索引 搜索的代码 @using K ...
- PC-用Windows XP自带的组策略加固操作系统
1.我的壁纸你别改==================================== 启动组策略时,单击“开始”按钮,选择“运行”命令,在“运行”文本框中输入“gpedit.msc”命令,即可启 ...
- Duff and Weight Lifting - 587A
题目大意:某个人训练举重,他每次可以举起来2^wi的重量,不过这个人比较懒所以他想尽量减少训练的次数,如果所有的训练重量2^a1 +2^a2+....2^ak = 2^x,那么这些重量可以一次性训练( ...
- hdu 2711&&poj2182 Lost Cows (线段树)
从后往前查第一个为0的奶牛肯定应该排在第一个.每次从后往前找到第一个为0的数,这个数应该插在第j位.查找之后,修改节点的值为极大值,当整棵树的最小值不为0的时候查找结束. 至于这种查找修改的操作,再没 ...
- JSP中解决获取请求参数中文乱码问题
分两种情况: 1.获取访问请求参数时乱码 解决方法:构造一个新的String String user = new String(request.getParameter("user" ...
- sql语句相关整理
select * from jcls_lawfirms where length(lf_2)=2 for updateselect * from jcls_lawfirms where length( ...
- UAF漏洞--iOS是越狱原理
Use After Free UAF 就是 Use After Free的缩写,是一种比较常见的内存错误式利用.很多iOS的越狱都是利用的这种方法.在此简单的举个例子说明UAF出现的情况 代码说明一切 ...
- android 中view的绘制过程
view的绘制过程中分别会执行:onMeasure(会多次)计算view的大小,OnLayout(),确定控件的大小和位置 onDraw()绘制view 当Activity获得焦点时,它将被要求绘制自 ...
- Repository模式介绍汇总
1.Linq To Sql中Repository模式应用场景 http://www.cnblogs.com/zhijianliutang/archive/2012/02/24/2367305.html ...
- Geogebra里给带有曲线和直线混合边界的封闭区域填充颜色
目的 用Geogebra绘制如图所看到的曲线,并填充如图边界的区域为实心: 用代码实现当然是能够的,可是,图形过于简单的时候用代码就不经济了.由于每个细小变动都还要调整改动代码并预览,非所见即所得.往 ...