在java中,volatile关键字解决的是变量在多个线程之间的可见性,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
(2)禁止进行指令重排序。
注:不了解“原子性,可见性和有序性”的同学可以看下笔者之前的博客
先看下下面的例子:
public class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
@Override
public void run(){
System.out.println("进入run了");
while(isRunning){
}
System.out.println("线程被停止了");
}
}
public class TestRunThread {
public static void main(String[] args){
try{
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已经将Running设置为false");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
线程一直在私有堆栈中取得isRunning的值是true,而代码thread.setRunning(false);虽然被执行,但其更新的却是公共堆栈的isRunning变量值为false,所以出现了死循环的状态,代码System.out.println(“线程被停止了”);从未被执行。如下图:
这个问题其实就是私有堆栈(上图的的“工作内存”)中的值和公共堆栈(上图中的“主内存”)中的值不同步造成的。解决这样的问题就需要使用volatile关键字了,它的作用就是当线程访问isRunning这个变量是,强制性从公共堆栈中取值。
将RunThread.java的变量isRunning用volatile关键字修饰,其他不变:
private volatile boolean isRunning = true;
重新运行,结果如下:
通过使用volatile关键字,强制从公共内存中读取变量:
这个例子说明了volatile关键字可以保证可见性(这也是volatile最重要的功能)
volatile关键字虽然保证了变量在多线程之间的可见性,但它却不具备同步性,所以也就不具备原子性。看下面这个例子:
public class MyThread extends Thread{
public volatile static int count;//volatile 修饰count
private static void addCount(){
for(int i=0; i<100; i++){
count++;
}
System.out.println("count=" + count);
}
@Override
public void run(){//实现run方法
addCount();
}
}
public class TestMyThread {
public static void main(String[] args) {
MyThread[] threadArray = new MyThread[100];
for(int i=0; i<100; i++){
threadArray[i] = new MyThread();//创建100个MyThread进程
}
for(int i=0; i<100; i++){
threadArray[i].start();
}
}
}
运行结果:
更改MyThread.java的addCount方法,将其用synchronized修饰,其他不变,
private synchronized static void addCount()
由此可以看出,volatile不保证原子性,否则运行结果应与synchronized修饰的结果一样。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。代码中的i++这种自增操作并不是一个原子操作,也就是非线程安全的。i++的操作步骤分解如下:
(1)从内存中取出 i 的值
(2)计算 i 的值
(3)将 i 的值写到内存中
假如在第(2)步计算值的时候,有另外一个进程修改了 i 的值,那么这个时候就会出现脏读(这就是上面运行结果count始终小于10000的原因)。解决的办法其实就是使用synchronized,所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存。
volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
volatile是怎么保证可见性和禁止指令重排序的?
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
下面将volatile与synchronized做一下比较总结:
(1)volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好
(2)volatile只能修饰变量,而synchronized可以修饰方法、代码块和类,新版本的jdk也对synchronized做了许多优化,开发中使用synchronized的比率比较大
(3)多线程访问volatile不会发生阻塞,而synchronized会发生阻塞
(4)volatile能保证可见性,但不能保证原子性,而synchronized都可以保证
(5)volatile解决的是变量在多线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。
《深入理解java虚拟机》 周志明
《Java多线程编程核心技术》高洪岩