volatile特性和内存语义
2015-10-02 15:58
375 查看
在多线程并发编程中,synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多线程开发中保证了共享变量的可见性。
原子性:对于任意单个volatile变量的读/写具有原子性,但是类似与volatileVal++这种复合操作来说,它就不具有原子性。
可见性:对于一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
原子性:这里强调的是对单个volatile变量的读或写具有原子性,这里的原子性其实是针对64位数据变量来说的,比如long和double。在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会带来比较大的开销。所以,为了照顾这种处理器,java语言规范鼓励但是不强制JVM对64位long型变量和double变量的写操作具有原子性。(大家注意,这里只是说写操作可以不具有原子性。因为从JDk5开始,java就要求任意的读操作都要具有原子性。)对于64位的写操作来说,它可以分为两个32位的总线读事务,那么,就会发生下面的情况:
从这里我们可以看出来,处理器B读取到的long变量其实是一个脏数据。如果我们用volatile来修饰这个变量,就会使它的写操作也具有原子性,就可以避免64位数据出现这种读写问题。
但是对于i++这种复合操作来说,volatile就显得无能为力了。i++其实分为三步:(这里的tmp其实是个抽象的概念)
tmp = i;
tmp = tmp + 1;
i = tmp;
我们看一下造成i++结果异常的一种情况:
这里假设A和B都同时执行i++操作,i初始值为0,那么两个i++操作过后,i的值为1而不是2.
当然,这里的值也可能为2,只要排序的顺序有变化的话。我们不能确定两个线程并发得执行i++操作后能够得到一个确切的结果,这就构成了一个线程安全问题。
可见性:大家都知道,cup的速度比内存数据读取的速度快的不只是一个数量级,如果每次都要从内存去读取数据的话,就会造成cpu资源的严重浪费,为了消除这种极大的不平衡,就出现了缓冲区这种东西。先把数据读取到缓冲区,cpu直接从缓冲区拿数据,然后在写回缓冲区,在适当的时间将缓冲区中的数据写回内存。
JMM定义了线程和主内存之间的抽象关系:线程中的共享变量存储在主内存中,每个线程都有一个私有的本地内存(是一个抽象概念,涵盖缓存、写缓冲、寄存器等),本地内存存储了共享变量的副本,线程直接对本地内存中的共享变量进行读写,并在适当的时候将共享变量写会主内存。这个适当的时候其实是不确定的,这就造成了数据的不一致性。比如说线程A对本地内存中的变量执行了写操作,但是她还没将本地内存中的副本刷回主内存,这时候线程B去主内存中读取这个变量的值的时候,就不能读取到最新的值,也就是说读取到了脏数据。
当我们把共享变量声明为volatile后,每当有线程写这个变量时,都会及时将本地内存中的值刷回主内存,然后将其他线程中对这个变量的缓存值设置为无效,当任意线程再去读取这个变量的时候,只能去主内存中读取这个变量,这样就保证了写操作对任意线程的可见性。
当对一个volatile变量进行写操作的时候,JMM会把该线程对应的本地内存中的共享变量的值刷新到主内存中。
当读一个volatile变量的时候,JMM会把该线程对应的本地内存设置为无效,要求线程从主内存中读取数据。
注意:这里并不是说把volatile变量刷新回主内存,而是说把所有共享变量(包括volatile变量)刷新回主内存
也就是说,当对volatile进行写操作之后,接下来任意线程对该volatile变量进行读操作的时候,都能看见volatile变量以及volatile变量之前语句(在程序中写在volatile写语句前面的语句)对其他共享变量的写。我们来看下面的例子:
这里的volatile内存语义保证了在(3)执行之前,(1)(2)所产生的影响已经刷新回主内存,而且线程对所有共享变量的读取都要去主内存中读,这就保证了b = 1; c = 0;
当第二个操作为volatile写操做时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序.这个规则确保volatile写之前的所有操作都不会被重排序到volatile之后;
当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序.这个规则确保volatile读之后的所有操作都不会被重排序到volatile之前;
当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序.这个规则和前面两个规则一起构成了:两个volatile变量操作不能够进行重排序
除开这三点之外的情况,可以进行重排序:
第一个操作是普通变量读/写,第二个是volatile变量的读
第一个操作是volatile变量的写,第二个是普通变量的读/写
推荐大家看一下这位大牛写的关于volatile正确使用的几种模式
volatile特性
volatile变量自身有两个特性:原子性:对于任意单个volatile变量的读/写具有原子性,但是类似与volatileVal++这种复合操作来说,它就不具有原子性。
可见性:对于一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
原子性:这里强调的是对单个volatile变量的读或写具有原子性,这里的原子性其实是针对64位数据变量来说的,比如long和double。在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会带来比较大的开销。所以,为了照顾这种处理器,java语言规范鼓励但是不强制JVM对64位long型变量和double变量的写操作具有原子性。(大家注意,这里只是说写操作可以不具有原子性。因为从JDk5开始,java就要求任意的读操作都要具有原子性。)对于64位的写操作来说,它可以分为两个32位的总线读事务,那么,就会发生下面的情况:
处理器A 处理器B (1)写long型变量的高32位 (2)读取整个long型变量 (3)写long型变量的低32位
从这里我们可以看出来,处理器B读取到的long变量其实是一个脏数据。如果我们用volatile来修饰这个变量,就会使它的写操作也具有原子性,就可以避免64位数据出现这种读写问题。
但是对于i++这种复合操作来说,volatile就显得无能为力了。i++其实分为三步:(这里的tmp其实是个抽象的概念)
tmp = i;
tmp = tmp + 1;
i = tmp;
我们看一下造成i++结果异常的一种情况:
线程A 线程B tmp = i; tmp = i; tmp = tmp + 1; i = tmp; tmp = tmp + 1; i = tmp;
这里假设A和B都同时执行i++操作,i初始值为0,那么两个i++操作过后,i的值为1而不是2.
当然,这里的值也可能为2,只要排序的顺序有变化的话。我们不能确定两个线程并发得执行i++操作后能够得到一个确切的结果,这就构成了一个线程安全问题。
可见性:大家都知道,cup的速度比内存数据读取的速度快的不只是一个数量级,如果每次都要从内存去读取数据的话,就会造成cpu资源的严重浪费,为了消除这种极大的不平衡,就出现了缓冲区这种东西。先把数据读取到缓冲区,cpu直接从缓冲区拿数据,然后在写回缓冲区,在适当的时间将缓冲区中的数据写回内存。
JMM定义了线程和主内存之间的抽象关系:线程中的共享变量存储在主内存中,每个线程都有一个私有的本地内存(是一个抽象概念,涵盖缓存、写缓冲、寄存器等),本地内存存储了共享变量的副本,线程直接对本地内存中的共享变量进行读写,并在适当的时候将共享变量写会主内存。这个适当的时候其实是不确定的,这就造成了数据的不一致性。比如说线程A对本地内存中的变量执行了写操作,但是她还没将本地内存中的副本刷回主内存,这时候线程B去主内存中读取这个变量的值的时候,就不能读取到最新的值,也就是说读取到了脏数据。
当我们把共享变量声明为volatile后,每当有线程写这个变量时,都会及时将本地内存中的值刷回主内存,然后将其他线程中对这个变量的缓存值设置为无效,当任意线程再去读取这个变量的时候,只能去主内存中读取这个变量,这样就保证了写操作对任意线程的可见性。
volatile内存语义
volatile内存语义有以下两点:当对一个volatile变量进行写操作的时候,JMM会把该线程对应的本地内存中的共享变量的值刷新到主内存中。
当读一个volatile变量的时候,JMM会把该线程对应的本地内存设置为无效,要求线程从主内存中读取数据。
注意:这里并不是说把volatile变量刷新回主内存,而是说把所有共享变量(包括volatile变量)刷新回主内存
也就是说,当对volatile进行写操作之后,接下来任意线程对该volatile变量进行读操作的时候,都能看见volatile变量以及volatile变量之前语句(在程序中写在volatile写语句前面的语句)对其他共享变量的写。我们来看下面的例子:
线程A 线程B (1) a = 1; (2) volatileVal = 0; (3) b = volatileVal; (4) c = a;
这里的volatile内存语义保证了在(3)执行之前,(1)(2)所产生的影响已经刷新回主内存,而且线程对所有共享变量的读取都要去主内存中读,这就保证了b = 1; c = 0;
volatile重排序
概括为以下三点(这三点之外的允许重排序):当第二个操作为volatile写操做时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序.这个规则确保volatile写之前的所有操作都不会被重排序到volatile之后;
当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序.这个规则确保volatile读之后的所有操作都不会被重排序到volatile之前;
当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序.这个规则和前面两个规则一起构成了:两个volatile变量操作不能够进行重排序
除开这三点之外的情况,可以进行重排序:
第一个操作是普通变量读/写,第二个是volatile变量的读
第一个操作是volatile变量的写,第二个是普通变量的读/写
慎用volatile
一些编程大牛往往会告诫我们说,尽量不要去使用volatile,因为使用volatile稍有不慎就会出现问题。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。推荐大家看一下这位大牛写的关于volatile正确使用的几种模式
相关文章推荐
- java的一些原则
- linux(ubuntu)中一些特殊符号
- JavaScript设计模式——方法的链式调用
- 布隆过滤
- JAVA实现调整数组顺序使奇数位于偶数前面问题(《剑指 offer》)
- Product of Array Except Self
- RGB颜色与灰度等变换关系
- 连单词成欧拉路 欧拉回路+字典树+并查集 POJ 2513 Colored Sticks
- zw版_Halcon图像交换、数据格式、以及超级简单实用的DIY全内存计算.TXT
- 【CF】7 Beta Round D. Palindrome Degree
- swift的类与对象讲解
- DM6437平台开发-----程序烧写2
- statickeyword于C和C++用法
- Linux 命令 - uniq: 通知或忽略重复行
- 使用Java 的Mongo API操作doc
- JavaScript权威指南学习之第5章 语句
- NLP | 自然语言处理 - 解析(Parsing, and Context-Free Grammars)
- PAT1008 数组元素循环右移问题
- 使用GPU在caffe上进行CNN训练
- iOS 多线程实例(自定义NSOperation并传值(block,notification))