volatile是什么
volatile在java语言中是一个关键字,用于修饰变量。被volatile修饰的变量后,表示这个变量在不同线程中是共享,编译器与运行时都会注意到这个变量是共享的,因此不会对该变量进行重排序。上面这句话可能不好理解,但是存在两个关键,共享和重排序。
变量的共享
先来看一个被举烂了的例子:
public class VolatileTest {
boolean isStop = false;
public void test() {
Thread t1 = new Thread() {
@Override
public void run() {
isStop = true;
}
};
Thread t2 = new Thread() {
@Override
public void run() {
while (!isStop) {
}
}
};
t2.start();
t1.start();
}
public static void main(String args[]) throws InterruptedException {
new VolatileTest().test();
}
}
(注:线程2中,while内容里如果写个System.out.prientln(""),导致循环退出,目前没明白什么原因。)
上面的代码是一种典型用法,检查某个标记(isStop)的状态判断是否退出循环。但是上面的代码有可能会结束,也可能永远不会结束。因为每一个线程都拥有自己的工作内存,当一个线程读取变量的时候,会把变量在自己内存中拷贝一份。之后访问该变量的时候都通过访问线程的工作内存,如果修改该变量,则将工作内存中的变量修改,然后再更新到主存上。这种机制让程序可以更快的运行,然而也会遇到像上述例子这样的情况。
存在一种情况,isStop变量被分别拷贝到t1、t2两个线程中,此时isStop为false。t2开始循环,t1修改本地isStop变量称为true,并将isStop=true回写到主存,但是isStop已经在t2线程中拷贝过一份,t2循环时候读取的是t2 工作内存中的isStop变量,而这个isStop始终是false,程序死循环。我们称t2对t1更新isStop变量的行为是不可见的。
如果isStop变量通过volatile进行修饰,t2修改isStop变量后,会立即将变量回写到主存中,并将t1里的isStop失效。t1发现自己变量失效后,会重新去主存中访问isStop变量,而此时的isStop变量已经变成true。循环退出。
volatile boolean isStop = false;
代码的重排序
再来看一个被举烂了的例子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
(注:感觉很难模拟,我没能模拟出来,也没找到他人的模拟结果)
如上代码示例,按照正常的想法,context初始化后,再把inited赋值为true。但是有可能有语句2先执行,再执行语句1的情况。导致线程2中doSomeThingWithConfig报错。因为jvm对代码进行编译的时候会进行指令优化,调整互不关联的两行代码执行顺序,在单线程的时候,指令优化会保证优化后的结果不会出错。但是在多线程的时候,可能发生像上述例子里的问题。如果上述的inited用volatile修饰,就不会有问题。
《深入理解Java虚拟机》中有一句话:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令生成一个内存屏障。保证重排序后的指令不会越过内存屏障,即volatile之前的代码只会在volatile之前执行,volaiter之后的代码只会在volatile之后执行。
volatile怎么用
volatile关键字一般用于标记变量的修饰,类似上述例子。《Java并发编程实战》中说,volatile只保证可见性,而加锁机制既可以确保可见性又可以确保原子性。当且仅当满足以下条件下,才应该使用volatile变量:
1、对变量的写入操作不依赖变量的当前值,或者确保只有单个线程变更变量的值。
2、该变量不会于其他状态一起纳入不变性条件中
3、在访问变量的时候不需要加锁。
逐一分析:
第一条说明volatile不能作为多线程中的计数器,计数器的count++操作,分为三步,第一步先读取count的数值,第二步count+1,第三步将count+1的结果写入count。volatile不能保证操作的原子性。上述的三步操作中,如果有其他线程对count进行操作,就可能导致数据出错。
第二条:
public class VolatileTest {
private volatile int lower = 0;
private volatile int upper = 5;
public int getLower() {
return lower;
}
public int getUpper() {
return upper;
}
public void setLower(int lower) {
if (lower > upper) {
return;
}
this.lower = lower;
}
public void setUpper(int upper) {
if (upper < lower) {
return;
}
this.upper = upper;
}
}
上述程序中,lower初始为0,upper初始为5,并且upper和lower都用volatile修饰。我们期望不管怎么修改upper或者lower,都能保证upper>lower恒成立。然而如果同时有两个线程,t1调用setLower,t2调用setUpper,两线程同时执行的时候。有可能会产生upper<lower这种不期望的结果。
测试代码:
public void test() {
Thread t1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
setLower(4);
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
setUpper(3);
}
};
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("(low:" + getLower() + ",upper:" + getUpper() + ")");
}
public static void main(String args[]) throws InterruptedException {
for (int i = 0; i < 100; i++) {
VolatileTest volaitil = new VolatileTest();
volaitil.test();
}
}
输出结果:
此时程序一直正常运行,但是出现的结果却是我们不想要的。
第三条:当访问一个变量需要加锁时,一般认为这个变量需要保证原子性和可见性,而volatile关键字只能保证变量的可见性,无法保证原子性。
最后贴个volatile的常见例子,在单例模式双重检查中的使用:
public class Singleton {
private static volatile Singleton instance=null;
private Singleton(){
}
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
new Singleton()分为三步,1、分配内存空间,2、初始化对象,3、设置instance指向被分配的地址。然而指令的重新排序,可能优化指令为1、3、2的顺序。如果是单个线程访问,不会有任何问题。但是如果两个线程同时获取getInstance,其中一个线程执行完1和3步骤,此时其他的线程可以获取到instance的地址,在进行if(instance==null)时,判断出来的结果为false,导致其他线程直接获取到了一个未进行初始化的instance,这可能导致程序的出错。所以用volatile修饰instance,禁止指令的重排序,保证程序能正常运行。(Bug很难出现,没能模拟出来)。
然而,《java并发编程实战中》中有对DCL的描述如下:"DCL的这种使用方法已经被广泛废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动很慢)已经不复存在了,因而它不是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。",其实我个小码畜的角度来看,服务端的单例更多时候做延迟初始化并没有很大意义,延迟初始化一般用来针对高开销的操作,并且被延迟初始化的对象都是不需要马上使用到的。然而,服务端的单例在大部分的时候,被设计为单例的类大部分都会被系统很快访问到。本篇文章只是讨论volatile,并不针对设计模式进行讨论,因此后续有时间,再补上替代上述单例的写法。