在多线程并发编程中,synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多线程开发中保证了共享变量的可见性。
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其实是个抽象的概念)
我们看一下造成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变量)刷新回主内存
也就是说,当对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 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。
推荐大家看一下这位大牛写的关于volatile正确使用的几种模式