应用场合及原理分析
随着多核CPU的普及,多线程的并发处理数据成为了人们提高程序速度的必要手段。但由于CPU中的高速缓存机制的影响,多线程对于数据的操作会出现数据读写不一致的情况,在JVM虚拟机中也是采用了同样的机制进行多线程的运行,对每个线程分配一定量的运行内存使其完成操作任务。JVM对数据的操作是分为三步的,例如下面这样的代码:
//线程1
int i = 0;
i++;
//线程2
i = i + 2;
这样的代码在CPU内核中,会先从主存中把i的值0取出来,然后在高速缓存中完成自增1的运算,最后回存入主存中。
因此在语言实现的过程中,必须有一种或多种对策来防止上述情况的发生。在java的语言实现中,提供了九种防止多线程的步伐不一致的对策:
- Synchronized
- CountDownLatch
- CyclicBarrier
- Exchanger
- Semaphore
- Phaser
- ForkJoinPool
- SwingWorker
- volatile变量
本文将介绍最后一种——volatile变量。
这种声明方法有两种作用同时也是实现原理:
- 保证不同的线程所看到的同一个变量的一致性。一旦某个线程更改了变量的值,别的线程会立即看到这个变量的新值。
- 禁止了指令的重排序。
比如下面这个例子:
//线程1
int i = 1;
while(i == 1)
{
stop();
}
//线程2
i++;
这个例子是用来实现中断进程的。但是这样的写法因为没有保护进程的操作,在线程1还在while循环中休眠时,线程2已经完成了对i的自增操作,这时线程1中运行内存里的i值还是1,它还在休眠,这样的程序就没有达到编写者的意图。如果对i变量加上了volatile声明如下:
//线程1
int volatile i = 1;
while(i == 1)
{
stop();
}
//线程2
i++;
这样,在线程中运行的变量就有点不一样了,会按顺序发生如下变化:
- 线程1和2将i=0读入自身的运行内存中。
- 线程1进入休眠
- 线程2完成对i的自增,i=2被立即写入主存中,同时使线程1运行内存中的缓存变量i=1无效。
- 线程1因为自身内存中的临时变量无效,就去主存中读取i的值,这时读到了i=2的最新值,因此结束休眠。
以上volatile关键字能够完成的,但是JVM里面有一个重要的原子性操作(原子性操作指对变量进行赋予真数字这样不能中断的操作)是volatile关键字不能完成的。比如这样一个程序:
public class test{
public volatile static int i=0;
public static void main(String[] args) {
for (int k = 0; k < 10; k++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 100; j++) {
i++;
}
});
thread.start();
}
System.out.println(i);
}
}
按照大众的期待,最后输出的结果应该是1000,可是这个程序的结果极小可能会是1000,原因正是volatile关键字不能保证原子性操作。假设一个线程A被创建出来,此时它已经有1个兄弟B在工作了,而i=10。前文说到过,JVM中对数据的操作分为三步:提取,改变,回存。若B已经提取到了i的值10而正在改变(自增1),A又去提取i的值10。这时,B的动作是比A提前的,因此会比A先回存i=11的值,但是A后来的动作又会把i=11的值再次回存一遍。这里,A和B的操作都是符合volatile的规范的,既没有隐瞒自己改数据的行为,也没有改变程序顺序。但是两步自增操作却使i只增加了1。这便是volatile不能实现原子性操作的原因。
实际应用举例
那么,volatile变量在java中有什么实际应用呢?
- 可以在多线程并发的处理环境中标志公用变量
volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
//线程1
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
//线程2
public void shutdownAfterTimeout()
{
while(timeout)
{
shutdownRequested = false;
}
}
如上述例子,如果一个变量在设计思路中需要同步的影响到多个线程,这个变量可以声明为volatile变量来简便地实现使此变量被多个线程同步的监听。
- 一次性安全发布
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble。
总结
volatile关键字是需要同步通知所有线程,又因内存开销的原因不便使用同步块的场合下的最优选。可以有效降低程序的内存使用,作为优化代码的一种常用的策略。
参考文献:
- Brian Goetz 《正确使用 Volatile 变量》https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
- 海子 《Java并发编程:volatile关键字解析》http://www.importnew.com/18126.html
作者:151142429xiehang