volatile 原理

volatile是一种弱同步机制,被修饰的变量操作,不会和其他内存操作一起重排序

用 volatile 修饰的变量具有两个重要特性:

  • 可见性:一个线程对该变量的修改,对于其他线程来说是可见的,每次线程获取该变量的值都是最新
    • 因此具有原子性:对任意单个 volatile 变量的读写具有原子性,其他运算操作并不具有原子性,比如 a++这种复合操作不符合
  • 禁止指令重排序优化

一、可见性和原子性

class Example {
    private boolean stop = false;
    public void execute() {
        int i = 0;
        System.out.println("thread1 start loop.");
        while(!getStop()) {
            i++;
        }
        System.out.println("thread1 finish loop,i=" + i);
    }
    public boolean getStop() {
        return stop; // 对普通变量的读
    }
    public void setStop(boolean flag) {
        this.stop = flag; // 对普通变量的写
    }
}
public class VolatileExample {
    public static void main(String[] args) throws Exception {
        final Example example = new Example();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.execute();
            }
        });
        t1.start();

        Thread.sleep(1000);
        System.out.println("主线程即将置stop值为true...");
        example.setStop(true);
        System.out.println("主线程已将stop值为:" + example.getStop());
        System.out.println("主线程等待线程1执行完...");

        t1.join();
        System.out.println("线程1已执行完毕,整个流程结束...");
    }
}

当 stop 没有被 volatile 修饰的时候,stop 的值就不会变为 true,因此thread1 不会结束

将 stop 设置为 volatile,thread1 就会获取到 stop 的值为 true

volatile 变量读写的内存实现

  • 当写一个 volatile 变量时,JVM 会将该线程对应的本地内存中共享变量值刷新到主存
  • 当读一个 volatile 变量时,JVM 会将该线程对应的本地内存设置为无效,从主存中读取共享变量

二、禁止指令重排序优化

1、什么是重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

  • 编译器优化的重排序,编译器在不改变单线程的程序语义的情况下,可以重新安排语句的执行顺序
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

2、双重检验锁的单例模式

当单线程或者单个 CPU 的时候,重排序不会带来问题。但是当在多线程的情况下, 就会出现数据不一致的现象。最典型的例子就是双重检验锁的单例模式:

   public class Singleton{

        private static  Singleton instance;

        private Singleton(){}

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

因为 instance = new Singleton{}这个操作并非是原子性的,在 JVM 中分为三个步骤:

1、给 instance 分配内存空间

2、使用 Singleton 的构造器来初始化变量

3、将 instance 对象指向分配的内存空间

在 JVM 指令重排序的情况下,这三步的执行顺序可能是可能是 1-2-3 也可能是 1-3-2。
当有多个线程调用 getInstance 方法时,如果第一个线程执行顺序是 132,当执行到 3 时,第二个线程进入,此时 instance 已经not null 了,但是还未初始化,此时第二个线程会直接返回 instance,就会报错。

3、lock相当于内存屏障,阻止后面的代码先执行

因此这里的解决方案就是,将 instance 用 volatile 来修饰。加入 volatile 之后,在汇编代码的赋值操作之后多了一个 lock 操作。

这个操作相当于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。这句指令中的“addl $0x0,(%esp)”(把ESP寄存器的值加0)显然是一个空操作(采用这个空操作而不是空操作指令nop是因为IA32手册规定lock前缀不允许配合nop指令使用),关键在于lock前缀,查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU或者别的内核无效化(Invalidate)其Cache,这种操作相当于对Cache中的变量做了一次前面介绍Java内存模式中所说的“store和write”操作。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。

这个涉及到内存屏障(Memory Barrier),内存屏障有两个能力:
a、就像一套栅栏分割前后的代码,阻止栅栏前后的没有数据依赖性的代码进行指令重排序,保证程序在一定程度上的有序性。
b、强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效,保证数据的可见性。

三、volatile 性能:

  • volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

  • volatile是轻量级同步机制
    相对于synchronized块的代码锁,volatile应该是提供了一个轻量级的针对共享变量的锁,当我们在多个线程间使用共享变量进行通信的时候需要考虑将共享变量用volatile来修饰。

  • volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。

四、volatile使用建议

  • 使用建议:在两个或者更多的线程需要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。

  • 由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

  • volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。

    当且仅当满足以下所有条件时,才应该使用volatile变量:

1、 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

2、该变量没有包含在具有其他变量的不变式中。

3、需要禁止重排序的情况下。

五、volatile和synchronized区别

1、volatile不会进行加锁操作:

volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。

2、volatile变量作用类似于同步变量读写操作:

从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。

3、volatile不如synchronized安全:

在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。

4、volatile无法同时保证内存可见性和原则性:

加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。