volatile深入实现原理分析与总结

1,051 阅读8分钟

轻量级同步机制volatile 关键字

volatile关键字用于修饰共享可变变量, 即没有使用final 关键字修饰的实例变量或静态变量, 相应的变量就被称为volatile变量, 如下所示: private volatile int logLevel;

volatile变量不会被编译器分配到寄存器进行存储, 对volatile变量的读写操作都是内存访问(访问高速缓存相当于主内存)操作。

​ volatile关键字常被称为轻量级锁, 其作用与锁的作用有相同的地方:保证可见性和有序性。 所不同的是. 在原子性方面它仅能保障写volatile变量操作的原子性, 但没有锁的排他性;其次,volatile关键字的使用不会引起上下文切换(这是volatile被冠以“轻量级的原因,volatile更像是一个轻量级简易(功能上有限)锁。

作用

volatile关键字的作用包括:保障可见性、 保障有序性和保障long/double型变最读写操作的原子性。

​ 但是, volatile仅仅保障对其修饰的变量的写操作(以及读操作)本身的原子性,而这并不表示对volatile变量的赋值操作一定具有原子性。例如,如下对volatile变量countl 的赋值操作并不是原子操作: ​ ​ countl = count2 + l; //read-modify-write

​ 一般而言, 对volatile变量的赋值操作, 其右边表达式中只要涉及共享变最(包括被赋值的volatile变量本身),那么这个赋值操作就不是原子操作。要保障这样操作的原子性,我们仍然需要借助锁。

volatile关键字在可见性方面仅仅是保证读线程能够读取到共享变量的相对新值。 对于引用型变量和数组变量,volatile关键字并不能保证读线程能够读取到相应对象的字段(实例变量、 静态变量)、元素的相对新值。

应用场景
  1. 使用 volatile变量作为状态标志。在该场景中.应用程序的某个状态由一个线程设置,其他线程会读取该状态并以该状态作为其计算的依据(或者仅仅读取并输出这个状态值)。此时使用volatile变量作为同步机制的好处是一个线程能够 通知 另外一个线程某种事件(例如,网络连接断连之后重新连上)的发生,而这些线程又无须因此而使用锁.从而避免了锁的开销以及相关问题,

  2. 场景二 使用 volatile保证可见性。

  3. 场景三 使用volatile变量替代锁。

    ​ volatile关键字并非锁的替代品 但是在一定的条件下它比锁更合适(性能开销小、代码简单)。多个线程共享一组可变状态变量的时候.通常我们需要使用锁来保障对这些变量的更新操作的原子性,以避免产生数据不一致问题,利用volatile变址写操作具有的原子性,我们可以把这一组可变状态变量封装成一个变量,对这些状态变量的更新操作就可以通过创建一个新的对象并将该对象的引用赋值给相应的引用型变量来实现。

  4. 使用它实现简易版读写锁。

实现原理:

在探究其实现原理之前,需要对内存屏障有一个认识,如果需要更深入可以翻看我之前的文章关于缓存一致性协议的机制,下面是关于内存屏障的介绍:

www.jianshu.com/p/b21082dd7…

以及缓存一致性协议:

www.jianshu.com/p/5e860ffd6…

然后来讲下关于java虚拟机是如何对volatile关键字的变量进行获取和插入屏障,从而保证编译后的顺序和执行顺序不会发生内存重排序。

Java虚拟机对synchronized、 volatile和final关键字的语义的实现就是借助内存屏障的。

​ 获取屏障相当于LoadLoad 屏障和Load Store屏障的组合, 它能够禁止该屏障之前的任何读操作与该屏障之后的任何读、写操作之间进行重排序。释放屏障相当于Load Store屏障和StoreStore屏障的组合,它能够禁止该屏障之前的任何读、 写操作与该屏障之后的任何写操作之间进行重排序。

volatile关键字的实现

​ Java虚拟机(JIT编译器)在 volatile变量写操作之前插入的释放屏障使得该屏障之前的任何读、写操作都先于这个volatile变量写操作被提交, 而Java虚拟机(JIT编译器) 在volatile变量读操作之后插入的获取屏障使得这个volatile变量读操作先于该屏障之后的任何读、写操作被提交。写线程和读线程通过各自执行的释放屏障和获取屏障保障了有序性。

​ 写线程、读线程通过释放屏障和获取屏障的这种配对使用保障了读线程对写线程执行的写操作的感知顺序与程序顺序一致, 即保障了有序性。

​ Java 虚拟机(JIT 编译器)会在volatile变量写操作之后插入一个StoreLoad屏障。该屏障不仅禁止该屏障之后的任何读操作与该屏障之前的任何写操作(包括该volatile 写操作) 之间进行重排序, 它还起到以下两个作用。

• 充当存储屏障。StoreLoad屏障是一个通用存储屏障,其功能涵盖了其他3 个基本内存屏障。StoreLoad屏障通过清空其执行处理器的写缓冲器使得该屏障前的所有写操作(包括volatile 写操作以及其他任何写操作)的结果得以到达高速缓存, 从而使这些更新对其他处理器而言是可同步的。

• 充当加载屏障, 以消除存储转发的副作用。假设处理器Processor 0 在/1时刻更新了某个volatile 变量, 在随后的!2时刻又读取了该变量。由于存储转发技术可能使得一个处理器无法将其他处理器对共享变量所做的更新同步到该处理器的高速缓存上, 而Java 语言规范又要求volatile 读操作总是可以读取到其他处理器对相应变量所做的更新, 因此Java 虚拟机需要在volatile变量写操作和随后的volatile变量读操作之间插入一个StoreLoad 屏障。这是利用了StoreLoad屏障既能够清空写缓冲器还能够清空无效化队列的功能,从而使其他处理器对volatile变量所做的更新能够被同步到volatile 变量读线程的执行处理器上。

Java 虚拟机(JIT编译器) 在volatile 变量读操作前插人的一个加载屏障相当于LoadLoad屏障, 它通过清空无效化队列来使得其后的读操作(包括volatile读操作)有机会读取到其他处理器对共享变量所做的更新。读线程能够读取到写线程对volatile变量最后所做的更新, 有赖于写线程在volatile 写操作后所执行的存储屏障。可见,volatile对可见性的保障是通过写线程、读线程配对使用存储屏障和加载屏障实现的。

加载屏障(loadload屏障) volatile变量读操作 获取屏障(LoadLoad 屏障和Load Store)

释放屏障 (Load Store屏障StoreStore) volatile变量写操作 store屏障

​ Java虚拟机对synchronized关键字的实现方式与对volatile 的实现方式类似。Java虚拟机在monitorenter (申请锁)字节码指令对应的机器码指令之后临界区开始之前的地方所插入的获取屏障以及在monitorexit(释放锁)字节码指令对应的机器码指令之前临界区结束之后的地方所插入的释放屏障确保了临界区中任何读、写操作无法被重排序到临界区之外, 这一点再加上锁的排他性确保了临界区中的操作成为一个原子操作。

最后附上代码的实现

/**
 * 一、volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。
 * 					  相较于 synchronized 是一种较为轻量级的同步策略。
 * 注意:
 * 1. volatile 不具备“互斥性”
 * 2. volatile 不能保证变量的“原子性”
 */
public class TestVolatile {

    public static void main(String[] args) {

        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();

        while (true) {
            //1.volatile 解决的是内存可见性问题,当子线程对Thread内部的一个属性进行修改时,
            //这时该属性对主线程来说时不可见的,也就是说主线程不能访问到该属性修改后的值。
            //有两种解决方法  第一种使用同步Synchronized 同步代码,会对不断该变量进行同步。如下:
           /* synchronized (threadDemo) {
                if (threadDemo.isFlag()) {
                    System.out.println("---------------");
                    break;
                }
            }*/
           //第二种:  设置属性为volatile
            if(threadDemo.isFlag()){
                System.out.println("----------------");
                break;
            }
        }
    }
}
class ThreadDemo implements Runnable{

    private volatile boolean flag=false;

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("flag=" + isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

整理总结不易,有收获请点个赞。谢谢