加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

java并发-4-volatile内存实现和内存屏障

(2018-04-13 16:14:00)
分类: java

一、           内存屏障

为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器重排序Java memory model把内存屏障指令分为4类:

http://s3/mw690/002i2qf8zy7jEXEokjEf2&690

这里所有的store都是刷新到内存而不是到缓存!所有的Load都是从主存中加载!

 

二、           Volatile的内存屏障实现

1.         Volatile的基本特性是:

1)  可见性

写入当前缓存立刻刷新到主存(总线lock指令),并告知持有该内存的其他缓存备份失效,从而强制重新读取主存。

2)  原子性

仅对单个volatile变量的读、写具有原子性

3)  有序性*

参加内存实现和增强有序性。

 

2.         Volatile的内存语义

JSR-133开始(即从JDK5开始),volatile变量的写-读可以实现线程之间的通信。

从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。

1)  当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效;

2)  当写一个volatile变量时,JMM会把线程本地内存写入主存;

线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程 发出了(其对共享变量所做修改的)消息。

线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。

线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过 主内存向线程B发送消息。

 

3.         Volatile的内存实现:

前文提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM 会分别限制这两种类型的重排序类型。表3-5JMM针对编译器制定的volatile重排序规则表:

http://s2/mw690/002i2qf8zy7jEXFJTjPf1&690

从表3-5我们可以看出。当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总 数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。

1)  volatile增加LoadLoad内存屏障指令,保证后面的读操作不会排在volatile读之前;

2)  volatile增加LoadStore内存屏障指令,保证后面的写操作不会排在volatile读之前;

3)  volatile增加StoreStore内存屏障指令,保证前面的写操作不会排在volatile写之前;

4)  volatile增加StoreLoad内存屏障指令,保证后面的读操作不会排在volatile写之前;

总之,volatile读操作内存屏障指令序列如下图2

http://s15/mw690/002i2qf8zy7jEXGkx3M8e&690

总之,volatile写操作内存屏障指令序列如下图3

http://s3/mw690/002i2qf8zy7jEXHjB2W82&690

所以,普通读/写仍有可能与volatile/写发生重排序。

例如图2位于volatile读之前的普通读/写都可以排到volatile读之后;图3位于volatile写之前的普通读可以位于volatile写之后(但被StoreLoad挡住),位于volatile写之后的普通写可以位于volatile写之前(但被StoreStore挡住)。

这种受限的重排序与表3-5中的设定不一致?

至于如何实现volatile可见性,假设有volatile写之后紧接着volatile读,那么由于内存屏障存在,读操作不可能位于写之前,又由于StoreLoad指令,保证写完后直接保存到内存而不是缓存,那么下面volatile读的时候就可以从主存重新读取到最新值(如何通过内存屏障指令通知其他共有缓存失效?)。

所以可以通过内存屏障限制指令重排序,从一定程度上保证可见性。

 

4.         为何需要增强Volatile的有序性(java5后增加如上内存屏障)

旧的java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java 存模型允许volatile变量与普通变量重排序。

http://s6/mw690/002i2qf8zy7jEXIgpdr05&690

如上例子,分别有两个线程进行writerreader,由于aflag没有直接依赖关系,可能会出现如下过程,导致线程B读到了未赋值的共享变量a

 

 

http://s6/mw690/002i2qf8zy7jEXJh0Il25&690

 

根据2volatile内存实现的增强后的volatile限制了volatile变量和普通变量的重排序,确保volatile-读和锁的释放-获取有相同的内存语义,一旦编译器发现重排序会破坏volatile的内存语义,就会禁止这种重排序。按照内存屏障指令的设定,此时上例中的执行顺序必然为1,2,3,4,没有发生任何重排序。

当然,编译器进行指令优化reordering时,并不是说volatile执行顺序与其他非volatile执行顺序不变,当然为了方便理解,可以近似认为volatile变量和普通变量不能重排序。

 

 

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有