Java中的关键字volatile是Java中提供的最轻量级的同步机制,那么为何可以在不加锁的情况下就可以用volatile来实现同步呢?
这要得益于volatitle关键字的两大特性:内存可见性、禁止重排序。
一.内存可见性
了解volatile是如何实现内存可见性之前,我们需要知道Java中主内存与工作内存的工作机制:
Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示。
也就是说我们使用的变量是工作内存中的变量,而不是直接拿主内存中的变量来使用,那么在多线程同时工作的情况下会产生什么样的问题呢?我们先看看下面的代码:
public class NotUseVolatile {
private static boolean stop=false;
public static void main(String[] args) throws InterruptedException {
Runnable runnable=new Runnable() {
@Override
public void run() {
while(!stop) {
System.out.println("runing");
}
}
};
new Thread(runnable).start();
//等待两秒后发出暂停信号
TimeUnit.SECONDS.sleep(2);
stop=true;
System.out.println("已发出暂停信号");
}
}
这段代码的意思是通过主线程发出暂停信号使得子线程停止打印,暂停的信号由一个boolean类型的变量stop表示,这个看似毫无问题的代码在某些特殊的情况下是无法正常运行的,比如当主线程更改了工作内存中的变量stop的值为true但是,并未立刻同步到主内存,或者是同步到主内存后子线程并未去主线程中获取最新的变量值,这都会导致主线程发出暂停信号而子线程继续工作的情况。那么该如何解决这个问题?想必大家都已经猜到了,那就是使用volatile来修饰我们的stop变量,volatile修饰的变量能够保证在多个线程中的可见性,也就是每个线程都能看到最新的值,当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此子线程再需要读取从主内存中去读取该变量的最新值。
二.禁止重排序
重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段,在单线程中我们感觉不到程序执行顺序的变化,因为Java内存模型保证了在单线程中指令重排序后执行的结果与顺序执行的结果是相同的。但是在多线程中指令重排序就可能会造成一些不可预知的错误。
线程 A
Config config=loadConfigFile(fileName);
//初始化
init(config);
//标记为已初始化
inited=true;
线程B
while(!inited) {
sleep();
}
doWork();
上面的代码描述的是一个非常常见的场景,然而这看似正常的代码却并无法正常工作,由于指令重排序的存在inited=true 可能会被重排序到 初始化前面,也就是还未完成初始化的情况下就发出了初始化完成的信号导致线程B误以为初始化完成而开始工作。所以我们可以通过使用volatitle关键字来修饰inited变量来禁止重排序。
Java内存模型对volatile语义有以下规定
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
最后需要注意的是,当volatile变量需要以自身的状态为基础来计算下一个状态的时候,比如说i++那么volatile无法保证在并发计算下的正确性,也就是说volatile变量并不能保证原子性。所以这种情况下还是需要使用synchronized来进行正确的同步或者是使用Java并发包下的原子类。