带你彻底了解volatile关键字,深入了解volatile关键字的作用

volatile

作用

保证可见性

/**
 * @author yeming.gao
 * @Description: volatile 可见性
 * @date 2020/6/1 13:47
 */
public class VolatileTest {

    private static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        //单独起一个线程修改flag的值
        new Thread(() -> {
            //睡眠3s
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false;
            System.out.println("flag 修改为false");
        },"change_flage_thread").start();

        int i = 0;
        while (flag) {
            i++;
        }
        System.out.println("第" + i + "次结束循环");
    }

}

该程序当属性flag不用volatile修饰的时候,即使在线程change_flage_thread将flag的值修改为false,这个程序依旧就处于无限死循环,当flag用volatile修饰的时候;在线程change_flage_thread将flag的值修改为false的时候,该程序的主线程while程序就会跳出,这个就是可见性。可见性:就是保证多线程情况下对属性的修改后的值其他线程也可以读取到。 volatile实现可见性主要包含三个步骤: 1,volatile修饰的共享变量属性修改后立即写到主内存 2,通知其他线程工作内存,使得线程工作内存volatile修饰的共享变量属性缓存无效。 3,读取volatile修饰的共享变量若缓存无效则重新从主内存中读取。 MESI缓存一致性协议   多个线程将同一个数据读取到各自的缓存区后,某个cpu修改了缓存的数据之后,会立马同步给主存,这都是汇编语言实现的。其他cpu通过总线嗅探机制(可以理解为监听)可以感知到数据的变化从而将自己缓存里的数据失效,从而去读取主存的值。以前的CPU可见性是利用lock锁,如今CPU就是使用MESI缓存一致性协议。锁的粒度更小。也是volatile可见性底层实现的关键。

不保证原子性

原子性 程序的原子性指整个程序中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节;原子性操作指原子性在一个操作是不可中断的,要么全部执行成功要么全部执行失败。在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

示例

/**
 * @author yeming.gao
 * @Description: volatile 原子性
 * @date 2020/6/1 13:47
 */
public class VolatileTest {

    private static volatile int a = 0;

    public static void main(String[] args) {
        //启动100个线程
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 10000; j++) {
                    a++;
                    System.out.println(Thread.currentThread().getName() + "中a=" + a);
                }
            }, "change_flage_thread_" + i).start();
        }
    }

}

上面程序执行完毕后,a的值不一定是1000000;所以说这块代码是线程不安全的。它之所以不安全就是因为volatile不能保证原子性。

防止重排序

概念 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

重排序的三种类型 image01

验证Java的重排序

/**
 * @author yeming.gao
 * @Description: 验证Java中存在指令重排序
 * @date 2020/6/5 9:51
 */
public class ReorderTest {

    private static int a = 0, b = 0;
    private static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {

        int whileCount = 0;
        do {
            whileCount++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread threadA = new Thread(() -> {
                a = 1;
                b = x;
            }, "线程A");

            Thread threadB = new Thread(() -> {
                x = 1;
                y = a;
            }, "线程B");
            threadA.start();
            threadB.start();
            threadA.join();//主线程等待threadA执行完毕
            threadB.join();//主线程等待threadB执行完毕

            System.out.println("循环第" + whileCount + "次b=" + b + ",y=" + y);
        } while (b != 0 || y != 0);
    }
}

分析:上面代码假设编译器没有指令重排序,那么b与y的值就不可能等于0;就单线程A来说,在不改变执行结果的情况下a = 1;b = x;的执行顺序可能变成b = x;a = 1;同样线程B也会发生这种情况,说一就会出现b=0;y=0的情况。这就可以说明发生了重排序。当属性a,b,x,y用关键字volatile进行修饰后禁止指令重排序后。b=0;y=0的情况就不可能出现,所以程序也就会一直循环下去。

实现方式

volatile通过加入内存屏障和禁止指令重排序优化来实现的:

Java内存模型中八种原子操作

下面我们分析一下双重锁的单例模式机制

/**
 * @author yeming.gao
 * @Description: 懒汉式单例模式-双重校验锁(线程安全);
 * @date 2020/5/14 17:41
 */
public class LazySingletonDoubleLock {
    private static volatile LazySingletonDoubleLock instance;

    /**
     * 私有化构造方法
     */
    private LazySingletonDoubleLock() {
    }

    public static LazySingletonDoubleLock getInstance() {
        if (instance == null) {
            synchronized (LazySingletonDoubleLock.class) {
                if (instance == null) {
                    instance = new LazySingletonDoubleLock();
                }
            }
        }
        return instance;
    }
}

java在new 实例化对象可以主要概括为三个步骤:

as-if-serial语义

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。譬如下面代码:步骤1,步骤2可能发生重排,但是步骤3不会发生重排(单线程情况下)。

步骤1,a=1
步骤2,b=x
步骤3,c=a+b

happens-before

在JMM中,使用happens-before的概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的happens-before规则如下: