提到并发,通常首先想到是锁,其实对共享资源的互斥操作是一方面,在Java中还有一方面是内存的可见性和顺序化,了解JMM的同学可能会更清楚些,内存可见性和顺序性同样非常重要,在这里简单提一下JMM模型,首先介绍一下SMP(对称多处理结构)如下图:

在计算机中缓存到处可见,我们知道cpu的运算速度非常快,而从内存、甚至磁盘的读取速度则相对慢了几个数量级,所以缓存起到的是一个缓冲的作用,提高cpu相对的运算效率。SMP中每个cpu都有自己的缓存并且对其他cpu不可见,同时多个cpu共同享有一个主内存,主内存还每个cpu的缓存通讯通过总线IO来实现,因此当cpu缓存中对于主存数据的副本改变时,要同步的通过IO总线来刷新主存的数据,保证其他cpu看见得数据是合法的。在JMM中每个线程都有自己的工作内存,对其他线程不可见,同时有一个主内存,共所有的线程共享,java中有个volatile关键字,是一种轻量级的同步,主要是用来实现内存的可见性。有volatile关键字修饰的变量,当在线程的工作内存发生变化的时候,会同时写回到主内存,其他线程读取的时候,也会强制从主内存重读,这就保证其他线程读到的数据是正确的。

下面看一张别人画的图:

上面就是提到的JMM模型,实际上每个线程都有自己的工作内存且只对自己可见,而这里的共享内存,一般指的也是java中的堆。

上面提到过,现代的处理器由于处理速度非常快,因此通常都会有一个写内存,先把值保存到自己的缓存中,找个合适的实际在刷新到共享内存,因此这里对内存的操作可能存在可见性的问题,举个例子:

Processor A Processor B
a = 1; //A1
x = b; //A2
b = 2; //B1
y = a; //B2
初始状态:a = b = 0
处理器允许执行后得到结果:x = y = 0

假设我们的代码如下:

a = 1;

b = 2;

x = b;

y = a;

其中a和b全为共享变量,可以理解是成员变量。

由于是多线程并发指向,完全可能出现上面表中的操作顺序。理论上即使是多线程也会得到x = 2;y = 1的结果(这里并没有数据争用),但是有可能会发生下面的情况:

处理器A(也可理解为线程A)的操作顺序是A1,A2,A3,处理器B的操作顺序是B1,B2,B3。

1.处理器A先把a=1写到自己的缓冲区,注意此时共享内存的a仍为0,于此同时处理B把b=2写到自己的缓冲区,但此时共享内存的b还是0。

2.处理器A从共享内存读取b的值,并赋值给x,于此同时处理器B从共享内存读取a的值,赋值给y。此时x = y = 0;

3.处理器A和B分别把自己缓冲区的值刷新到共享内存。

从代码层面看处理器A质性的是A1->A2,但是从内存可见性看,执行完刷新共享内存a的写入才算完成。因此这里的实际质性顺序是A2->A1,因此这里的指令被重排序了。因为大多数啊处理器都应用到了写缓冲区,所以重排序的特性很常见。

JMM针对这种重排序的特性会生成内存屏障指令来阻止某种程度的重排序,从而保证内存的可见性,cpu为了提高执行速度,会对我们的代码(编译后生成的指令)进行重排序,因此代码的执行顺序并不重要,只要我们的最终结果正确就行,因此有时候为了程序的正确性,jvm不得不作出某些动作来保证结果的可见性,这其中包括下面几种:

屏障类型 指令示例 说明
LoadLoad Barriers Load1; LoadLoad; Load2 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
StoreStore Barriers Store1; StoreStore; Store2 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
LoadStore Barriers Load1; LoadStore; Store2 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。
StoreLoad Barriers Store1; StoreLoad; Load2 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

以第一个load-load为例子,该指令确保load1的操作在load2及其之后的所有load操作被执行前,执行,且保证load的值对所有的处理器可见。

实际上java中的voilate原语就是阻止对指令的重排序,volatile变量在写操作之后会插入一个store屏障,在读操作之前会插入一个load屏障。(也就是说对于volatile变量,如果有线程修改了它的值,该值会马上对其他线程可见,并且一个线程读取该值的时候,其他线程缓存中的值会被同步刷新到最新值)。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。因此volatile使用需要谨慎,用的不好会造成性能的浪费(频繁的通过总线刷新各个处理器的值,可能造成数据风暴)。

为了简化这种可见性,java中有个

程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 
监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 
volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。 
传递性:如果A happens- before C,那么A happens- before C。 
Thread.start()的调用会happens-before于启动线程里面的动作。 
Thread中的所有动作都
解释一下第一条,在单线程中,一个线程的操作对该操作后续的所有操作都可见(注意
上面只是列了几个规则,实际可能不止这些,如果不满足上面的规则,则需要考虑使用同步等方法,来强制满足。

另外上面的几个规则是基于java的内存模型给出的,在java语言层面也给出了很多
happens-before简化了并发编程的难度,了解它的含义多少对我们有些好处。附上一张java的内存模型图:

jvm的happens-before原则的更多相关文章

  1. JVM内存调优原则及几种JVM内存调优方法

      转载,如需帮助,请联系wlgchun@163.com https://blog.csdn.net/LeegooWang/article/details/88696195 如何对JVM进行内存调优? ...

  2. 深入理解JVM(六) -- GC执行原则和方案

    上篇文章中,我们了解了Java虚拟机垃圾回收的思路和策略,这篇文章我们将了解Java是如何实现高效的回收算法的. 我们需要了解,内存回收必须要保证“一致性”,意思就是在执行GC分析的时候,系统看起来要 ...

  3. JVM虚拟机瓜分内存原则

    操作系统分配给每个进程的内存是有限制的,例如32位的Windows限制为2GB.虚拟机提供了参数来控制java堆和方法区(非堆)这两部分内存的最大值.则剩余的内存为2GB(操作系统限制)减去Xmx(最 ...

  4. JVM探索之内存管理(三)

    上节我们介绍了JVM垃圾回收的原则,还有几个垃圾收集算法:标记-清除算法.复制算法.标记整理算法.分代收集算法:现在将要说HotSpt的垃圾收集器,这小节将只是理论. Java虚拟机规范对垃圾收集器的 ...

  5. java虚拟机--jvm client模式与server模式的区别

    JVM Server模式与client模式启动,最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升.JVM如果不显式指定是-Server模式还是-clien ...

  6. JVM入门

    面试过程中,问到JVM一脸懵逼,在github看了一些文章,感觉质量不错,整理了一下希望大家不吝赐教. 目前主流的jdk采用解释与编译混合执行的模式,其JIT技术采用分层编译,极大地提升了Java的执 ...

  7. JVM内存结构与垃圾回收总结

    1.JVM内存模型 JVM只不过是运行在你系统上的另一个进程而已,这一切的魔法始于一个java命令.正如任何一个操作系统进程那样,JVM也需要内存来完成它的运行时操作.记住:JVM本身是硬件的一层软件 ...

  8. 99.9%的Java程序员都说不清的问题:JVM中的对象内存布局?

    本文转载自公众号:石彬的架构笔记,阅读大约需要8分钟. 作者:李瑞杰 目前就职于阿里巴巴,资深 JVM 研究人员 在 Java 程序中,我们拥有多种新建对象的方式.除了最为常见的 new 语句之外,我 ...

  9. SpringBoot Quickstart

    SpringBoot Intro SpringBoot是顺应现在微服务(MicroServices)理念而产生的一个微框架(同类微框架可供选择的还有Dropwizard), 用来构建基于Spring框 ...

  10. 1002-谈谈ELK日志分析平台的性能优化理念

    在生产环境中,我们为了更好的服务于业务,通常会通过优化的手段来实现服务对外的性能最大化,节省系统性能开支:关注我的朋友们都知道,前段时间一直在搞ELK,同时也记录在了个人的博客篇章中,从部署到各个服务 ...

随机推荐

  1. HTMLTestRunner显示用例打印内容

    我们知道默认的HTMLTestRunner运行时成功只会显示...,失败也只是显示E suite = unittest.TestLoader().loadTestsFromTestCase(MyTes ...

  2. DML操纵语句

    --在新增数据的时候,如果在表名之后没有跟 列名,那么values()必须写全--顺序必须不能改变,这个顺序就是表中列的顺序insert into dept values(70,'20','哈哈') ...

  3. SQL 中nvarchar和varchar到底能存多少汉字和英文

    实践出真知,在SQL中,nvarchar(50)表示汉字和英文都是50,varchar(50)汉字25,英文50

  4. Fiddler+Firefox

    配置置代理了,发现还是不好用!无法抓包: 配置就是在firefox的“选项”,拉到最下面,就能够看到“网络代理”,点进去:手动代理里面输入Fiddler的代理信息(默认127.0.0.1:8888) ...

  5. requireJS多页面应用实例

    本文是requireJS的一些知识点的总结,配上多页面应用中的实例分析. 本案例的目录结构如下: requireJS API的三个主要函数:define(创建模块),require(加载模块),con ...

  6. jredis 客户端 使用

    redis学习及实践3---Jedis.JedisPool.Jedis分布式实例介绍 Java中使用Jedis操作Redis Redis客户端:Jedis

  7. 数据运算+-*/,比较运算符==!=,赋值运算,逻辑运算and,or,not,成员运算in,not in,身份运算is is not,位运算&|,运算符的优先级

    取模就是返回余数. 取模的作用主要是来取奇偶数来用的,奇数干嘛,偶数干嘛. 比较运算符: 赋值运算: 逻辑运算: 赋值的时候可以多个变量同时赋值 成员运算: in就是在不在的意思. 身份运算: 位运算 ...

  8. 第四章.使用ant编译hadoop eclipse插件

    从hadoop 0.20.203以后,hadoop的发布包里,不再对eclipse插件进行jar包发布,而是给出了打包的代码,需要各位开发人员自己进行打包和设置.我们打的包必须跟自己使用的hadoop ...

  9. submit提交表单

    <!DOCTYPE html><html><head> <script src="jquery-1.3.2.min.js">< ...

  10. Mysql--可用的 MySQL 产品和专业服务

    一.MySQL Community Edition(社区版):MySQL Community Edition is the freely downloadable version of the wor ...