您的位置:首页 > 其它

volatile特性和内存语义

2015-10-02 15:58 375 查看
在多线程并发编程中,synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多线程开发中保证了共享变量的可见性。

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正确使用的几种模式
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: