一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
#####volatile关键字能保证可见性和有序性,但是不保证原子性。因此并不能保证线程安全。
看一个相关的例子:双重校验锁实现的单例模式:
public class DoubleCheckSymbol {
private static volatile DoubleCheckSymbol d;
private DoubleCheckSymbol() {}
public static DoubleCheckSymbol getSymbol() {
if (d == null) {
synchronized(DoubleCheckSymbol.class) {
if (d == null) {
d = new DoubleCheckSymbol();
}
}
}
return d;
}
}
这个单例模式中为什么要加volatile关键字呢?
如果不加volatile的话,会有如下隐患:
d = new DoubleCheckSymbol()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1, 给d分配内存。
2, 调用 DoubleCheckSymbol的构造函数来初始化成员变量。
3, 将d对象指向分配的内存空间(执行完这步 d就为非 null 了)。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,但是由于它并未初始化,所以就可能发生错误。
此例中,synchronized关键字已经解决了原子性问题。同时也解决了可见性问题,因为synchronized能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
然而有序性问题并没有解决,所以,这就是在这里使用volatile的目的,即为了防止指令顺序的重排序。
另外,如果在JDK1.5之前这样用volatile的话,可能会出现异常结果。此前的JDK中即使将变量声明为volatile也不能完全避免重排序导致的问题(主要是volatile变量前后的代码仍然存在重排序问题)。
再看一个例子:
package com.lwc.test;
import java.util.concurrent.CountDownLatch;
public class Counter {
private static volatile int value;
private static CountDownLatch countDownLatch = new CountDownLatch(10000);
public static void main(String[] args) throws Exception{
for (int i=0;i<10000;i++){
new Thread(){
@Override
public void run() {
increment();
countDownLatch.countDown();
}
} .start();
}
countDownLatch.await();
System.out.println(getValue());
}
public static int increment(){
return value ++;
}
public static int getValue(){
return value;
}
}
输出:可能是10000,也可能是小于10000的数,同样是因为volatile不能保证原子性(value ++并不是原子性操作,所以会出现两个线程同时取得了相同的value值,然后分别+1,然后各自写入内存,结果value只增加了1的情况)。
更详细的解释,可以参考前人总结:
http://www.cnblogs.com/dolphin0520/p/3920373.html