【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 ...
随机推荐
- bzoj3831 [Poi2014]Little Bird 单调队列优化dp
3831: [Poi2014]Little Bird Time Limit: 20 Sec Memory Limit: 128 MBSubmit: 505 Solved: 322[Submit][ ...
- VS2012中C++,#include无法打开自己所写的头文件(.h)
最近刚开始学cocos2d-x,创建项目之后,自己按照<cocos2d-x 3.x 游戏开发>的教程写代码 先写了一个头文件 MyHelloWorldScene.h 然后在 AppDe ...
- 关于惠普hp服务器开机时F10菜单变成F10 Function Disabled的解决方法
今天笔者由于在Intelligent Provisioning智能配置里不小心将"启动Intelligent Provisioning"选项钩选成禁用了,如下 结果就造成,在之后服 ...
- Linux下的crontab定时、执行任务命令详解 oracle 自动备份
在LINUX中,周期执行的任务一般由cron这个守护进程来处理[ps -ef|grep cron].cron读取一个或多个配置文件,这些配置文件中包含了命令行及其调用时间.cron的配置文件称为&qu ...
- Cisco 的基本配置实例之四----vlan的规划及配置(接入交换机)
4.2 接入交换机的相关配置 ## 在此例中,我们联入的是一台接入交换机,此交换机的gi0/1口上联至核心交换机.也就意味着我们需要配置gi0/1为trunk口.具体的配置如下: D-2960-3(c ...
- 简介JSP与FreeMarker及Volicity区别
FreeMarker FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页.电子邮件.配置文件.源代码等)的通用工具. 它不是面向最终用户的,而是一个 ...
- Nginx配置二级目录/路径 映射不同的反向代理和规避IP+端口访问
当配置Nginx来映射不同的服务器 可以通过二级路径来反向代理 来解决一个外网端口实现多个服务访问. 配置如下: server { listen ; server_name demo.domai ...
- 基于Windows服务器,从0开始搭建一个基于RTSP协议的直播平台
作案工具下载 EasyDarwin 服务端程序,用来接受推流和拉流 FFmpeg 可以用来推流视频数据到服务端,也可以从服务端拉流下来播放,也可以从一个服务端拉流下来,转推到另一个服务端去. Easy ...
- 如何找回Oracle所有用户丢失的密码
如何找回Oracle所有用户丢失的密码: 方法一:1.以操作系统验证的方式登录 SQL>conn / as sysdba; 2.查看系统中的用户名. SQL>select USERNAME ...
- Android APT(编译时代码生成)最佳实践
越来越多第三方库使用apt技术,如DBflow.Dagger2.ButterKnife.ActivityRouter.AptPreferences.在编译时根据Annotation生成了相关的代码,非 ...