Java关键字之volatile(可见性,有序性)

一. volatile关键字是什么?

当一个变量定义为volatile之后,它将具备两种特性:
  ①保证此变量对所有线程的可见性
    当一条线程修改了这个变量的值,新值对于其他线程可以说是可以立即得知的。Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存保存了该线程使用到的变量在主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量。《深入理解Java虚拟机第二版》P363
Java内存模型
  ②禁止指令重排序优化
    普通的变量仅仅会保证在该方法的执行过程中所有依赖该赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一直。在一个线程的方法执行过程中无法感知到这点,故Java内存模型描述所谓的“线程内表示为串行的语义”《深入理解Java虚拟机第二版》P369

二. volatile两种特性的体现


  ①保证此变量对所有线程的可见性

/**
 * 〈volatile关键字特性测试〉
 *
 * @author 龙
 * @create 2018/9/7 15:21
 * @since 1.0.0
 */
public class Volatile {
    public static volatile int count=0;
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(//开启10个线程
                    () -> {
                        for(int j=0;j<100;j++){
                            count++;//每个线程执行加100
                        }
                        System.out.print(count+" ");
                    }
            ).start();
        }
    }
}
//运行结果:
//130 130 230 330 430 588 588 688 788 888
//100 200 300 400 500 600 700 800 900 1000
//103 103 203 303 403 503 603 703 803 903

    上面的结果理想的结果是1000,然而却出现了其它的结果,在并发执行的过程中,哪怕一次结果不对就认为这是不安全的。count++操作看似只有一条指令,但是在Java虚拟机层面却已经是几条指令的组合,如读取count,载入count,加一,存储count,写入count到主内存中。假设线程A在载入count之后,线程B也载入了count,两个线程分别加一再写回主内存,count就写入了两个相同的值,本应该是加二却只是加一。注意这并不违背可见性,毕竟在B线程读取count的时候,A线程并没有改变count的值,则B线程可以说依然读取的是count的正确的结果。

那么我们是否可以对count的加一操作进行同步已达到正确的结果那?

/**
 * 〈volatile关键字特性测试〉
 *
 * @author 龙
 * @create 2018/9/7 15:21
 * @since 1.0.0
 */
public class Volatile {
    //此处Volatile关键字可有可无,synchronized关键字保住了可见性
    public static Integer count=0;
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(
                    () -> {
                        for(int j=0;j<100;j++){
                            add();//原子操作
                        }
                        System.out.print(count+" ");
                    }
            ).start();
        }

    }
    public static synchronized void add(){
        count++;//保证count加一的操作是原子的
    }
}
//运行结果:
//200 300 400 200 500 600 700 800 900 1000 
//100 200 300 437 500 600 700 800 900 1000 
//100 200 300 465 500 600 700 800 900 1000 

    可以说此时的结果已经是正确的了,原子性的加一操作就可以实现线程安全。

  ②禁止指令重排序优化

volatile boolean isOK = false;
//假设以下代码在线程A执行 
A.init();
isOK=true;
//假设以下代码在线程B执行
while(!isOK){
	sleep();
}
B.init();

    A线程在初始化的时候,B线程处于睡眠状态,等待A线程完成初始化的时候才能够进行自己的初始化。这里的先后关系依赖于isOK这个变量。如果没有volatile修饰isOK这个变量,那么isOK的赋值就可能出现在A.init()之前(指令重排序,Java虚拟机的一种优化措施),此时A没有初始化,而B的初始化就破坏了它们之前形成的那种依赖关系,可能就会出错。

三. 什么样的情况使用Volatile关键字?


  ①确保它们自身状态的可见性,如单例模式的双重检查加锁实现
  ②标识一些重要的程序生命周期时间的发生(初始化或者关闭),如上述初始化代码
  ③再贴出一些Volatile的使用指南:volatile用法指南




参考资料:
  《深入理解Java虚拟机第二版》
  《Java并发编程实战》

posted @ 2018-09-07 18:09  李子君啊  阅读(586)  评论(0编辑  收藏  举报