关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确、完整地理解,以至于许多程序员都习惯不去使用它,遇到需要处理多线程数据竞争问题的时候一律使用synchronized来进行同步。

当一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。

volatile变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的,我们可以通过一段简单的演示来说明原因,请看以下代码清单中演示的例子。

/**

  • volatile变量自增运算测试
  • @author zzm

*/
public class VolatileTest {

public static volatile int race = 0;

public static void increase() {
    race++;
}

private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
    Thread[] threads = new Thread[THREADS_COUNT];
    for (int i = 0; i < THREADS_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increase();
                }
            }
        });
        threads[i].start();
    }

    // 等待所有累加线程都结束
    while (Thread.activeCount() > 1)
        Thread.yield();

    System.out.println(race);
}

}
这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能够正确并发的话,最后输出的结果应该是200000。读者运行完这段代码之后,并不会获得期望的结果,而且会发现每次运行程序,输出的结果都不一样,都是一个小于200000的数字。

问题就出现在自增运算“race++”之中,我们用Javap反编译这段代码后会得到以下代码清单,发现只有一行代码的increase()方法在Class文件中是由4条字节码指令构成的(return指令不是由race++产生的,这条指令可以不计算),从字节码层面上很容易就分析出并发失败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中。

public static void increase();
Code:
Stack=2,Locals=0,Args_size=0
0:getstatic#13;//Field race:I
3:iconst_1
4:iadd
5:putstatic#13;//Field race:I
8:return
LineNumberTable:
line 14:0
line 15:8
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。

1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2.变量不需要与其他的状态变量共同参与不变约束。
而在像如下的代码清单所示的这类场景就很适合使用volatile变量来控制并发,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来。

volatile boolean shutdownRequested;
public void shutdown(){

shutdownRequested=true;

}
public void doWork(){

while(!shutdownRequested){
    //do stuff
}

}


Smile3k
197 声望22 粉丝