参考博文:https://www.hollischuang.com/archives/2648
一、volatile简介
Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、以及JUC包等。
volatile 这个关键字,不仅仅在Java语言中有,在很多语言中都有的,而且其用法和语义也都是不尽相同的。尤其在C语言、C++以及Java中,都有 volatile 关键字。都可以用来声明变量或者对象。使用volatile修饰的变量有两层语义:保证内存可见性和禁止指令重排序。
下面简单来介绍一下Java语言中的 volatile 键字。
二、volatile的用法
volatile 通常被比喻成 ”轻量级的synchronized“ ,也是Java并发编程中比较重要的一个关键字。和 synchronized 不同,volatile 是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
volatile 的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile 修饰就可以了。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
上述代码,是一个典型的使用双重校验锁实现的单例模式,其中,使用volatile 关键字修饰可能被多个线程访问的单例对象singleton。
三、volatile的原理
为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。
但是,对于volatile 变量,当对volatile 变量进行写操作的时候,JVM会向处理器发送一条 lock 前缀的指令,将这个缓存中的变量回写到系统主存中。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
所以,如果一个变量被volatile 所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile 在并发编程中,其值在多个缓存中是可见的。
四、voaltile与可见性
可见性是指:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中所用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程A改了某个变量的值,但是线程B不可见的情况。
前面的关于volatile 的原理中介绍过了,Java中的volatile 关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以 使用volatile 来保证多线程操作时变量的内存可见性。
示例:
package basis.stuJUC.sutVolatile;
public class TestVolatile extends Thread {
public static void main(String[] args) throws Exception{
MyThread thread = new MyThread();
thread.start();
System.out.println("输入任意字符结束子线程。。。");
System.in.read();
thread.flag = true;
}
}
class MyThread extends Thread{
public boolean flag = false;
@Override
public void run() {
System.out.println("子线程开始执行。。。");
while (true){
if (flag){
break;
}
}
System.out.println("子线程结束。。。");
}
}
当子线程(MyThread类)中的 flag 不使用 volatile 修饰的时候,运行程序并输入任意字符并不能结束正在执行的子线程,因为在主线程中修改 flag 的值,子线程并不能看到这个值的修改。
在 flag 前面加上volatile 关键字后,运行程序,输入任意字符后可以正常结束子线程。
public volatile boolean flag = false;
五、volatile与有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如 load->add->save 有可能被优化成load->save->add 。这就是可能存在的有序性问题。
而volatile 除了可以保证数据的可见性之外,还有一个强大的功能,那就是保证代码的有序性,他可以禁止指令重排序,也就是所禁止JVM对volatile修饰的代码进行优化。
普通的变量仅仅会保证,在该方法的执行过程中,所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。
volatile可以禁止指令重排序,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile 修饰的变量的操作,会严格按照代码顺序执行,load->add->save 的执行顺序就是:load、add、save。
在 “二、volatile的用法“ 中,使用双重校验锁实现单例模式,synchronized 关键字已经保证了单例对象的可见性,那为什么还需要使用volatile 关键字修饰呢,这里就是为了防止JVM在单例对象进行初始化的时候,对该单例对象进行优化,防止指令重排序,保证其有序性。
六、volatile与原子性
原子性是指:一个操作是不可中断的,要全部执行完成,要不就都不执行。
线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
synchronized关键字为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile 和这两个指令之间是没有任何关系的。
所以,volatile
是不能保证原子性的。
示例:
package basis.stuJUC.sutVolatile;
public class TestVolatile_1 {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final TestVolatile_1 test = new TestVolatile_1();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
System.out.println(Thread.currentThread().getName()+"执行完毕");
};
}.start();
}
while(Thread.activeCount()>2) { //保证前面的线程都执行完
Thread.yield();
}
System.out.println(test.inc);
}
}
上述代码创建10个线程,然后分别执行1000次i++
操作。正常情况下,程序的输出结果应该是10000,但是,多次执行的结果都小于10000。为什么会出现这种情况呢,那就是因为虽然volatile可以保证 inc 在多个线程之间的可见性。但是无法保证 inc++ 的原子性。