深入理解Volitile关键字
Volatile
概念:JVM提供的一个轻量级的同步机制
作用:
- 防止JVM对 Long/Double 等64位的非原子性协议进行的 误操作(读取半个数据);
- 可以使某一个变量对所有的线程立即可见(某一个线程如果修改了工作内存中的变量副本,那么加上Volatile关键字之后,该变量就会立即同步到其他线程的工作内存当中)。
- 禁止指令 "重排序" 优化。
前面两点在之前的稳文章中都有提到,下面我们来看什么是指令"重排序"。看指令重排序之前,首先要理解什么是原子性!
原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
- 
原子性 num = 10; 就是一个原子操作,这段代码在程序的底层就这么一句话,不会再拆开了! 
- 
非原子性 int num = 10; 如果现在先定义变量,再赋值。这个操作就是非原子性的。 这段代码在程序底层会拆分成两步,这两步已经不能再拆分了,所以是原子性的。 1、int num; 2、num = 10; 
重排序
为了性能优化,编译器和处理器会进行指令重排序。排序的对象就是 原子性 操作!
比如上面的例子,int num = 10不是原子性操作。所以程序会在底层将它变成 int num 和 num = 10,把它变成原子性后在进行重排序。
下面通过一个例子理解重排序,有一个直观的映像:
int a = 10;		//1  int a; a= 10;
int b;			//2
b = 20;			//3
int c = a * b;	//4
重排序不会影响单线程的执行规则,因此以上程序在经过重排序后,可能的执行过程为1234或者2314,1234就是按照上面正常的执行流程,2314为
int b;			//2
b = 20;			//3
int a = 10;		//1
int c = a * b;	//4
在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下:
/**
 * @author leizige
 */
public class Singleton {
    private Singleton(){
    }
    public  static Singleton instance = null;
    public static Singleton getInstance() {
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    /* 不是一个原子性操作 */
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
以上代码在并发环境中会出现问题,原因是 instance = new Singleton()不是一个原子性操作,在执行过程中会拆分为一下几步:
- JVM会为 instance 分配内存地址以及内存空间。
- 在执行时通过构造方法实例化对象。
- 将 instance 指向在第一步分配好的内存地址 。
根据我们前面重排序的知识,以上代码在真正执行时可能是 1、2、3,也可能是 1、3、2。
如果在多线程环境下,使用1、3、2可能会出现问题:
假设线程A刚执行了1、3步骤,但还没有执行2,此时 instance 已经指向了JVM分配的内存地址。如果现在线程B进入 if(instance == null) ,会直接拿到 instance 的对象(此instance是刚才线程A并没有new的对象)。这时拿到的 instance 对象是null,如果直接使用必然会报错!
解决方案就是添加 Volatile 关键字来禁止 程序使用1、3、2的重排序顺序。
public volatile static Singleton instance = null;
Volatile 是否能保证变量的原子性、 线程安全
不能!
下面通过一段代码来验证一下:
/**
 * @author leizige
 */
public class TestVolatile {
    private volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        /**
         * 每个线程num++300次,100个线程在线程安全时,结果应该为300w;
         */
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 30000; j++) {
                    /* 不是一个原子性操作 */
                    num++;
                }
            }).start();
        }
        /**
         * 这里不能直接打印num,代码当中开了100个线程,main也是一个线程。
         * 假如100个线程,每个线程执行需要5ms
         * 但是从main方法开始到打印num,可能只需要花2ms
         * 如果main线程执行完,子线程还没执行完,所以会发生错误
         * 所以需要先暂停1s,让子线程执行完
         */
        Thread.sleep(1000);
        System.err.println(num);
    }
}
以上代码执行结果与预期的300w不符,下面我们分析一下线程不安全的原因:
其实造成原因的代码还是 num++,这句代码不是一个原子性操作。
num++ 等价与 num = num +1;
num = num +1 还可以拆分为以下两步:
- num + 1;
- num = 第一步的结果;
假设两个线程在执行时通过执行 num + 1;(假设此时num的值为10)
线程A执行 10 +1 = 11;
线程B执行 10 +1 = 11;
正常执行完线程A和B之后num的值应该为12,在并发环境下可能出现两个线程同时+1,就造成了漏加的情况,所以结果与预期不符合。
如何将 num 变成原子性的呢,只要使用 java.util.concurrent.atomic包下的 AtomicInteger。该类能够保证原子性的核心是因为提供了compareAndSet()方法,该方法提供了 CAS算法(无锁算法)。
/**
 * @author leizige
 */
public class TestVolatile {
    //    private volatile static int num = 0;
    private static AtomicInteger num = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        /**
         * 每个线程num++300次,100个线程在线程安全时,结果应该为300w;
         */
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 30000; j++) {
//                    num++
                    /* 一个原子性操作 */
                    num.incrementAndGet();
                }
            }).start();
        }
        System.err.println(num);
    }
}

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号