【java多线程系列】java内存模型与指令重排序
在多线程编程中,需要处理两个最核心的问题,线程之间如何通信及线程之间如何同步,线程之间通信指的是线程之间通过何种机制交换信息,同步指的是如何控制不同线程之间操作发生的相对顺序。很多读者可能会说这还不简单,java中的同步采用的是锁机制或volatile来完成的,的确,在应用层,java中的同步的确是通过加锁来完成的,但是锁机制是如何实现的呢?这就涉及到java中的内存模型的相关知识。本博客将带领大家了解java内存模型的相关知识。
如果读者觉得本博客写的不错,记得小手一抖,点个赞哦!另外欢迎大家关注我的博客账号哦,将会不定期的为大家分享技术干货,福利多多哦!
我们知道java中多线程通信采用的是共享内存模型,即多个线程之间共享某块内存,通过写-读内存中的公共状态进行隐式通信,整个通信过程对于程序员完全透明,因此理解java内存模型将帮助我们理解这种隐式通信的原理,从而更好的写出java多线程程序。
一java内存模型的抽象结构:
我们知道在java中,对象实例域,静态域和数组元素存储在堆内存中,堆内存在线程之间共享,我们称对象实例域,静态域和数组元素为共享变量,而局部变量,方法定义的参数和异常处理器参数不会在线程之间共享,它们不存在内存可见性的问题,因此不受java内存模型的影响。
java线程之间的通信受java内存模型(Java Memory Model,简称JMM)的控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见,从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,而每个线程各自拥有属于自己的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。注意本地内存是一个抽象概念,在物理设备上不存在,它通常包含缓存,写缓冲区,寄存器以及其他的硬件和编译器优化等。java内存模型的抽象示意图如下:
从图可知,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
即java线程之间的通信必须经过主内存,JMM通过通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
二指令序列的重排序:
前面说过每个线程拥有自己的本地内存(一个抽象的概念,多个物理设备内存的抽象),其中一种就是硬件和编译器优化。在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排序,之所以把这个拿出来讲,是因为我们知道CPU将按照指令序列执行指令,如果指令被重排序,那么对线程的读写会产生影响,这就会影响我们前面提到的java内存模型。所以接下来就介绍一下重排序,重排序包括3种类型
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对 应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
其中第一种很好理解,这是保证程序顺序执行最基本的原则,第二条中的如果数据不存在依赖关系这点给大家解释一下,示例代码如下:
int x=1;
int y=x+1;
int z=1;
因为第二行语句中y=x+1,即y的结果依赖于x的值,那么y与x存在依赖关系,而z与x与y不存在依赖关系,所以在指令重排序后x必须始终在y的前面出现,而z与x与y之间的关系可以乱序,即重排序后结果可以为:
int z=1;
int x=1;
int y=x+1;
但不能为:
int z=1;
int y=x+1;
int x=1;//x赋值必须在y之前
上述3种重排序可能会导致多线程程序出现内存可见性问题,对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序,对于处理器,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序
三java内存模型内存屏障指令
前面说过,常见的处理器都会对程序指令进行重排序,而这在多线程中很可能导致内存可见性问题,而java内存模型确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。这也是java内存模型根本作用。而禁止重排序的方法就是插入内存屏障指令,为了更好的理解为何需要禁止重排序,我们先来看一个例子:
假设处理器A和处理器B按程序的顺序并行执行内存访问,最终却可能得到x = y = 0的结果。具体的原因如下图所示:
这里对这个图稍作一下解释,因为写缓冲区仅对自己的处理器可见,所以虽然处理器A已经在缓冲区A中更新了a的值,但是处理器B不能感知到,因此处理器B从内存中读取a的值赋给y时,如果此时处理器A还未将a的值刷新到内存中,那么此时内存中a的值仍然为0,这样y的值就为0,同理x的值可能为0,而这显然不是我们所期望的结果,
之所以出现上述结果是因为现代的处理器都会使用写缓冲区来临时保存向内存写入的数据,这相信大家在计算机组成原理这么课中都学过,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致
我们知道对内存的操作包括读-写两种,那么多线程访问同一个共享变量则两两组合共四种情况,现代常见处理器的重排序对这四种组合允许情况如下所示:
上图中“N”表示处理器不允许两个操作重排序,“Y”表示允许重排序。我们可以看出:常见的处理器都允许Store-Load重排序;常见的处理器都不允许对存在数据依赖的操作做重排序。(注意上图所说的x86包括x64及AMD64。)
与上图对应java内存模型定义了四种禁止重排序的四种指令屏障,如下图所示:
java内存模型通过这四种内存屏障指令来保证了前面我们所举的例子的情况不会出现,仍然以上述例子来说明,Java内存模型通过在适当位置插入内存屏障指令,如StoreLoad Barriers指令,则可以保证Store1数据对其他处理器是可见的(即将缓存中的内容刷新到内存),这样在处理器A将a的值=1写入缓冲区A后将及时保证处理器B在从内存中读取a的值之前会将处理器A缓存中的值刷新到内存中。从而保证内存可见性。
注意:StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。
以上就是本博客的主要内容,java内存模型主要解决多线程程序中的内存可见性问题,该内容是理解java多线程编程的理论基础。
如果读者觉得本博客写的不错,记得小手一抖,点个赞哦!另外欢迎大家关注我的博客账号哦,将会不定期的为大家分享技术干货,福利多多哦!
【java多线程系列】java内存模型与指令重排序的更多相关文章
- Java多线程中的内存模型
转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6536131.html 一:现代计算机的高速缓存 在计算机组成原理中讲到,现代计算机为了匹配 计算机存储设备的 ...
- [心得笔记]Java多线程中的内存模型
一:现代计算机的高速缓存 在计算机组成原理中讲到,现代计算机为了匹配 计算机存储设备的读写速度 与 处理器运算速度,在CPU和内存设备之间加入了一个名为Cache的高速缓存设备来作为缓冲:将运算需要 ...
- JVM并发机制的探讨——内存模型、内存可见性和指令重排序
并发本来就是个有意思的问题,尤其是现在又流行这么一句话:“高帅富加机器,穷矮搓搞优化”. 从这句话可以看到,无论是高帅富还是穷矮搓都需要深入理解并发编程,高帅富加多了机器,需要协调多台机器或者多个CP ...
- Java并发编程-线程可见性&线程封闭&指令重排序
一.指令重排序 例子如下: public class Visibility1 { public static boolean ready; public static int number; } pu ...
- 内存可见性,指令重排序,JIT。。。。。。从一个知乎问题谈起
在知乎上看到一个问题<java中volatile关键字的疑惑?>,引起了我的兴趣 问题是这样的: package com.cc.test.volatileTest; public clas ...
- (Java 多线程系列)java volatile详解
在前面的文章里面介绍了synchronized关键字的用法,这篇主要介绍volatile关键字的用法. Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其它 ...
- (Java 多线程系列)java synchronized详解
synchronized简介 Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block).同步代码块包括两部分:一个作为锁对象的引用,一个作为由这个锁保护的代码块. ...
- (Java 多线程系列)Java 线程池(Executor)
线程池简介 线程池是指管理同一组同构工作线程的资源池,线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务.工作线程(Worker Thread)的任务很简单 ...
- 面试官:小伙子,你给我讲一下java类加载机制和内存模型吧
类加载机制 虚拟机把描述类的数据从 Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制. 类的生命周期 加载(Loadi ...
随机推荐
- [BZOJ]4755: [Jsoi2016]扭动的回文串
Time Limit: 10 Sec Memory Limit: 512 MB Description JYY有两个长度均为N的字符串A和B. 一个"扭动字符串S(i,j,k)由A中的第i ...
- hdu 5887 搜索+剪枝
Herbs Gathering Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)T ...
- POJ1509 Glass Beads
Glass Beads Time Limit: 3000MS Memory Limit: 10000K Total Submissions: 4314 Accepted: 2448 Descr ...
- bzoj4946 Noi2017 蔬菜
题目描述 小 N 是蔬菜仓库的管理员,负责设计蔬菜的销售方案. 在蔬菜仓库中,共存放有nn 种蔬菜,小NN 需要根据不同蔬菜的特性,综合考虑各方面因素,设计合理的销售方案,以获得最多的收益. 在计算销 ...
- TensorFlow官方文档
关于<TensorFlow官方文档> <TensorFlow官方文档>原文地址:http://devdocs.io/tensorflow~python/ ,本次经过W3Csch ...
- SQL之排序
1.按多个列排序 经常需要按不止一个列进行数据排序.例如,如果要显示雇员名单,可能希望按姓和名排序(首先按姓排序,然后在每个姓中再按名排序).如果多个雇员有相同的姓,这样做很有用. 要按多个列排序,简 ...
- BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
这个坑爹的玩意 有几个出现错误的原因 服务器 1.服务器重复启动同一个部署 这个时候要停止然后启动 电脑差的 重启电脑 重启服务器就好了 代码 2.bean工厂不知道哪里关闭 3.bean工厂没有找到 ...
- spring cloud 入门系列二:使用Eureka 进行服务治理
服务治理可以说是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务实例的自动化注册和发现. Spring Cloud Eureka是Spring Cloud Netflix 微服务套件的一部分 ...
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
- jsp&servlet初体验——用户登录功能实现
数据库准备-创建db_login数据库 t_user表 1.创建web工程 2.创建用户model user.java package com.gxy.model; public class U ...