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与可见性的更多相关文章

  1. Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)

    JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...

  2. 多线程并发之java内存模型JMM

    多线程概念的引入是人类又一次有效压寨计算机的体现,而且这也是非常有必要的,因为一般运算过程中涉及到数据的读取,例如从磁盘.其他系统.数据库等,CPU的运算速度与数据读取速度有一个严重的不平衡,期间如果 ...

  3. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  4. 全面理解Java内存模型(JMM)及volatile关键字(转)

    原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...

  5. 对多线程java内存模型JMM

    多线程概念的引入体现了人类重新有效压力寨计算机.这是非常有必要的,由于所涉及的读数据的过程中的一般操作,如从磁盘.其他系统.数据库等,CPU计算速度和数据读取速度已经严重失衡.假设印刷过程中一个线程将 ...

  6. 深入理解Java内存模型JMM与volatile关键字

    深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...

  7. Java并发编程:Java内存模型JMM

    简介 Java内存模型英文叫做(Java Memory Model),简称为JMM.Java虚拟机规范试图定义一种Java内存模型来屏蔽掉各种硬件和系统的内存访问差异,实现平台无关性. CPU和缓存一 ...

  8. 深入理解Java内存模型JMM

    本文转载自深入理解Java内存模型JMM JMM基础与happens-before 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执 ...

  9. 什么是Java内存模型(JMM)

    什么是java内存模型 缓存一致性问题 在现代计算机中,因为CPU的运算速度远大于内存的读写速度,因此为了不让CPU在计算的时候因为实时读取内存数据而影响运算速度,CPU会加入一层缓存,在运算之前缓存 ...

随机推荐

  1. 在Windows Server 2012服务器上安装可靠多播协议

    为什么要安装可靠多播协议?   答:随着因特网的发展,出现了视频点播.电视会议.远程学习.计算机协同工作等新业务.传统的点到点通信方式,不仅浪费大量的网络带宽,而且效率很低.一种有效利用现有带宽的技术 ...

  2. 问题-[Delphi]提示Can't load package:dclite70.bpl解决方法

    问题现象:提示Can't load package:dclite70.bpl 问题原因:全是Window2003的Data Execution Prevention(DEF数据执行保护)造成的. 解决 ...

  3. 引入less报错解决方法以及浏览器设计不同的地方

    XMLHttpRequest cannot load file:///C:/Users/PAXST/Desktop/805/first.less. Cross origin requests are ...

  4. python selenium启动浏览器打开百度搜索

    python selenium打开百度搜索 #!usr/bin/python from selenium import webdriver import time browser = webdrive ...

  5. lettCode-Array

    1   Remove Element    lintcode-172 描述: 删相同元素,反现有长度 记忆:标不同元素,反标记值 public int removeElement(int[] a, i ...

  6. Hyper-V介绍

    Hyer-v主机是高端虚拟主机用户的最佳选择.您不再受其他用户程序对您造成的影响,您将得到的是更加公平的资源分配,远远低于虚拟主机的故障率.Hyper-V的分区包含两种:父分区和客户分区.Hyper- ...

  7. 分布式助手Zookeeper(四)

    分布式助手Zookeeper(四)博客分类: Zookeeper zookeeper配置同步zookeeper编程 Zookeeper是分布式环境下一个重要的组件,因为它能在分布式环境下,给我带来很多 ...

  8. 在不同平台上CocosDenshion所支持的音频格式

    在大多数平台上,cocos2d-x调用不同的SDK API来播放背景音乐和音效.CocosDenshion在同一时间只能播放一首背景音乐,但是能同时播放多个音效. 背景音乐 Platform supp ...

  9. hdu1016JAVA

    import java.util.Arrays;import java.util.Scanner;public class Main { public static int kase=0,n; pub ...

  10. fuck'em

    不要说GUNGHO的游戏,连逆转三国这种都没玩过,还是做手游的,表现的那么冠冕堂皇,还不只是个常规的做软件的而已.只是以做软件的程度来做游戏,能做出的是个JB.