volatile解析

156 阅读3分钟

前言

volatile变量是Java提供的一种削弱的同步机制,用来确保将变量的更新操作通知到其他线程。我将在这篇博客中分享自己对volatile变量的理解。

volatile特点

  • 保证被修饰的变量不会被重排序
  • 保证被修饰变量可见性

什么是重排序

重排序是指当操作之间不存在依赖关系(Happens-Before关系)时,编译器在不改变单线程程序最终执行结果的前提下,对指令进行重排序以达到提高执行效率的目的。如下面代码中所示:

int a = 0;
int b = 2;
int c = a + b

代码中a=0和b=2之间不存在依赖关系(Happens-Before关系),即他们的执行顺序不会影响最终的执行结果,所以b=2可能会被排序到a=0之前执行。但是a=0和b=2不会被重排序到c=a+b之后执行,因为他们之间存在依赖关系(Happens-Before关系)。

为什么要防止重排序

在单线程情况下,重排序可以给程序带来更高的执行效率。但是在有些情况下重排序就会带来不可预测的问题。如下面代码所示。

public class PossibleReordering{
    static int num = 0;
    static boolean flag = false;
    
    public static void main(String[] args) throws InterruptedException{
        Threadd one = new Thread(new Runnable(){
           public void run(){
               while(!flag){
               }
               System.out.println(num);
           } 
        });
        
        one.start();
        num = 44;    \\ 语句1
        flag = true; \\ 语句2
    }
}

在没有重排序的情况下会正常输出44,但如果语句1和语句2交换了顺序,则输出0。因此重排序在某些情况下会导致线程安全问题。

可见性问题

在多线程的情况下回导致可见性问题,如下图所示,线程A和线程B都拥有各自的工作内存,工作内存是线程私有的,即对其他线程不可见。主存中存在一个共享变量a=0,且线程A和线程B的工作内存中都保存了共享变量a=0的副本。如果共享变量并没有被volatile变量修饰,那么线程A或线程B对于工作内存中共享变量a的副本的修改可能不会马上刷新到主存中,也就是说线程A的工作内存中的副本a可能已经被修改成了5,但这些变化对于线程B并不可见。

可见性问题.png

如下图所示,如果共享变量a是volatile变量,则线程A对于副本的修改会立即刷新到主存中并通知其他线程,被volatile修饰的变量可以保证对于变量的写操作位于读操作之前。

可见性.png

volatile解决不了原子性问题

当线程A和线程B同时要对主存中的共享变量a进行加1操作时,会首先将共享变量a读取到自己的工作内存中,即此时线程A和线程B的工作内存中都存储了共享变量a=0的副本。然后线程A和线程B分别对各自工作内存中a变量的副本进行加1操作,然后写回主存中,主存中a变量的最终结果是1,但a实际上被进行了两次加一操作。

原则操作问题.png