字数很多,仔细阅读,从一个初学者,第一次接触volatile视角解释理解该概念。相信对你有所帮助

Volatile关键字的作用是:保证可见性。禁止指令重排序。

保证可见性

  • 当一个线程修改了 volatile 变量的值,其他线程能立即看到最新值
     
            boolean flag = false;
    
            new Thread(() -> {
    
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                flag = true;
                System.out.println("线程1启动");
            }).start();
    
            new Thread(() -> {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程2启动:" + flag);
            }).start();
    
            new Thread(() -> {
                int i = 0;
                while (!flag) {
                    i++;
                }
                System.out.println("线程3停止:"+i);
            }).start();
    
        }

    上面有三个线程,定义了一个变量(没有volatile修饰)。运行结果如下:

可以看到线程1启动了,并且将flag的值修改为true,线程2得到了修改后的flag的值:true。

但是线程3的flag仍然是flase。这是为什么呢?

解释:因为线程1在休眠100毫秒之后修改flag的值为true,线程2在休眠200毫秒之后的会重新刷新自己线程的工作内存,得到了就是新的flag。但是线程3并没有休眠,所以它拿到的就是最开始的flag,并且将最开始的flag值放到自己的工作内存中,并不知道线程1已经修改了。

ps:JMM(java内存模型)规定了线程有自己的工作内存和共享内存,自己的工作内存是其他线程不可见的,而共享内存是所有线程可见的。在操作数据的时候,会首先将共享内存中的数据拷贝到自己的私有内存中然后再操作。

这里可能有一个概念JIT,即时编译器。JIT会对重复执行的代码进行优化,将其升级为局部变量,升级为机器码。这样就变成了死循环。但并不能说是因为JIT导致了变量不可见问题,而是JIT让这个问题变得明显了,主要是因为线程内存的独立性。

禁止指令重排序

首先我们解释一下为什么出现指令重排序问题。这是一种优化,Java 内存模型(JMM)允许编译器和处理器对指令进行重排序以优化性能。可以看一下代码,下面就是重排序,在单线程情况下没有任何影响,在一些情况下,重排序执行可以优化我们的代码执行效率。

int a= 1;  ①
int b =2;  ②
int c= 3;  ③
// 上面三行代码, 如果按顺序执行是①②③。实际上我们的代码可能是先执行② - ③- ①
// 虽然顺序变了,但是结果并没有任何影响

但是在多线程情况下就不一样了。

看一下下面的代码,线程A和线程B并行执行。线程A执行的时候发生了重排序,我们以为的执行顺序是 a= 1; b =2 ; c= 3 (1-2-3); 实际顺序变成了 c = 3; a =1 ;b =2;(3-1-2)。当线程A顺序变成了(3-1-2)的时候,c=3赋值完成之后,线程B也执行到了if(c==3) 这样就得到结果为true,而这个时候a,b还没赋值呢。最终输出的结果就是0,0(默认值)。 

int a = 0;
int b = 0;
int c = 0;

// 线程 A
public void writer() {
    a = 1;      // (1)
    b = 2;      // (2)
    c = 3;      // (3)
}

// 线程 B
public void reader() {
    if (c == 3) {       //  读
        System.out.println(a); // 0
        System.out.println(b); // 0
    }
}

出现了上面这情况是我们想避免的,这个时候就需要使用volatile关键字了。这个关键字可以让线程B执行到c==3的时候,a,b一定是赋值的状态。解释一下:

volatile关键字并不能阻止重排序,我们使用了volatile关键字之后,并不是把顺序执行强行修改为了(1-2-3),线程A中的代码执行顺序可能还是(3-1-2)。这个关键字的作用是当线程B执行到(c==3)的时候,a和b一定是赋值完成的状态。也就是说线程A,线程B同时执行,假设线程A执行顺序是3-1-2,那么执行完c=3的时候,线程B也无法得到c==3,必须等Volatile关键字修饰的变量之前的写操作都完成之后(也就是a,b都赋值之后),线程B才能得到c==3。这样就能保证线程B执行的时候ab都是有值的。

--------------------使用volatile---------------------
int a = 0;
int b = 0;
volatile int c = 0;

// 线程 A
public void writer() {
    a = 1;      // (1)
    b = 2;      // (2)
    c = 3;      // (3)
}

// 线程 B
public void reader() {
    if (c == 3) {       //  读
        System.out.println(a); // 2
        System.out.println(b); // 3
    }
}

底层逻辑:为什么线程A已经给c赋值了,但是线程B无法得到c==3的值呢?

JVM 会在 c = 3 之前插入 StoreStore 屏障,确保:

  • a = 1 和 b = 2 的写操作 先于 c = 3 刷入主存或缓存
  • CPU 不会把 c = 3 的写入“提前”让其他核心看到,而 ab 还在写缓冲区(Store Buffer)里。
 缓存一致性协议保证原子可见性

在多核 CPU 中:

  • 线程 A 所在的核心在执行 volatile 写时,会触发缓存行的 flush 和 invalidate 广播
  • 但这个广播只有在所有前置写操作完成后才发出
  • 所以线程 B 所在的核心不可能先看到 c=3,却看不到 a=1 或 b=2

也就是说即使c先执行完了,因为加了一个屏障,其他线程看不到,必须等a,b都执行完了其他线程才能看到。

图片中讲的:写操作加的屏障是阻止上方其他写操作越过屏障排序到volatile变量之下。这里说的是逻辑上的不能,只要别的线程看不到volatile修饰的变量先赋值,那么就是其他写操作在Volatile写操作之上。但是底层物理写还是会重排序的。

而“读操作加的屏幕是阻止下方其他写操作越过屏障操作到volatile变量读之上”。这句话的意思,如果你在读取的时候,需要Volatile的禁止重排序读,那么就必须先读取c(volatile修饰的)且建立因果关系,然后在读取a,b,这样获取到的a,b才是有值的,否则a,b可能是0,0。

为什么必须建立因果关系?可以参考线程C,虽然线程C先读取了c再读取a,b。但是可能存在线程A还没赋值给c,这个时候线程C执行了,那么这个时候的c就等于0。a,b也是等于0。

而带了条件判断(因果关系),就只有当c==3的时候,那么a,b一定有值且值正确。

int a = 0;
int b = 0;
volatile int c = 0;

// 线程 A
public void writer() {
    a = 1;      // (1)
    b = 2;      // (2)
    c = 3;      // (3)
}

// 线程 B
public void reader() {
        if(c==3){

            System.out.println(b); 
            System.out.println(a);
    
        }
        
}
-------------------------
// 线程 C
public void reader() {
             

            System.out.println(c); 
            System.out.println(b); 
            System.out.println(a);
    
        }
        
}

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐