1. 前言

volatile这个关键字比较重要,尤其是在看多线程的时候,会经常出现。那么就来看下这个关键字到底有什么用。

volatile这个关键字的引入是为了线程安全,但是volatile不保证线程安全。

在多处理器的系统中(或者单处理器多核的系统),每个处理器(每个核)都有自己的高速缓存,而它们有共享同一主内存(Main Memory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。 为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。

多核环境下,如果存在一个核的计算任务依赖另一个核 计的算任务的中间结果,而且对相关数据读写没做任何防护措施,那么其顺序性并不能靠代码的先后顺序来保证,处理器最终得出的结果和我们逻辑得到的结果可能会大不相同。

线程安全有3个因素:可见性、有序性、原子性。线程安全是指在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改而发生不期望的情况。

2. volatile的作用:

volatile有3个作用

2.1 保证可见性;

使用volatile来解决多核CPU高速缓存导致的变量不同步问题。

本质上这是个硬件问题,其根源在于:CPU的高速缓存的读取速度远远快于主存(物理内存)。所以,CPU在读取一个变量的时候,会把数据先读取到缓存,这样下次再访问同一个数据的时候就可以直接从缓存读取了,显著提高了读取的性能。而多核CPU有多个这样的缓存。这就带来了问题,当某个CPU修改了变量,但是其他的CPU在修改前已经将该数据读取到了自己的缓存,当其他CPU再次读取数据的时候,它仍然回去自己的缓存区域中读取,此时读取到的值仍然是修改前的值,但实际上这个值已经做了修改,这里就涉及到了线程安全的要素:可见性

可见性是指当多个线程在访问同一个变量时,如果其中一个线程修改了变量的值,那么其他线程应该能够立即看到修改后的值。

volatile 的中文意思是不稳定的,易变的,用 volatile 修饰变量是为了保证变量的可见性。

volatile 的实现原理是内存屏障,其原理为:当CPU写数据时,如果发现一个现有变量在其他CPU存有副本,就会发出信号通知其他CPU将该副本对应的缓存行置为无效状态,当其他CPU读取到变量副本的时候,会发现该缓存行是无效的,然后就将重新从主存中读取。

简单案例:

private static int number = 0;


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


    new Thread(()->{
        System.out.println("进入子线程中,开始运行子线程...");
        while(number == 0){

        }
    }).start();

    TimeUnit.SECONDS.sleep(2); // 睡眠2秒

    number++;
    System.out.println("主线程中: number is " + number);
}

结果:
在这里插入图片描述
也就是说,在主线程中,当我们修改了number这个值,但是子线程中却不知道我们做了修改,也就是这个变量对其他线程不可见。这显然不应该。

前面提到使用volatile关键字可以保证可见性,这里测试下:

private static volatile Integer number = 0;

在这里插入图片描述

2.1.1 内存屏障

是被插入两个 CPU 指令之间的一种指令,有两个作用:

  • 用来禁止处理器指令发生重排序(像屏障一样),从而保障有序性的。
  • 该指令会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障可见性

2.2 volatile 还可以解决指令重排序问题

Java中重排序通常是在编译器或运行时环境,为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序运行期重排序,分别对应编译时和运行时环境。同样的,指令重排序不是随意重排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果,即编译器(和处理器)需要保证程序能够遵守“串行” as-if-serial 属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。
  • 存在数据依赖关系的不允许重排序。

也就是说在CPU运行期间会对指令进行优化,没有依赖关系的指令,它们的顺序可能会被重排。在单线程环境中,发生重排是没有问题的,CPU保证了顺序不一定一致,但结果一定一致。

但是在多线程环境下,重排则会引起很大的问题,这涉及到线程安全的要素:有序性

有序性是指程序执行的顺序应该按照代码的先后顺序执行。

具体一点解释,禁止重排序的规则如下:

  • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  • 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

2.3 volatile不能保证操作的原子性

原子性(Atomicity) 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

也就是一个或多个操作,要么全部连续执行且不会被任何因素中断,要么就都不执行。这个概念和数据库概念里的事务很类似,且事务就是一种原子性操作。

需要注意的是,volatile可以保证线程安全的可见性和有序性,但不保证操作的原子性。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐