2024-03-23
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1091860717

回答

volatile 是 Java 提供的一种轻量级的同步机制,与 synchronized 修饰方法、代码块不同,volatile 只能用来修饰变量。当一个变量被声明为 volatile 后,它会确保所有线程看到该变量的值都是一致的,即一个线程更新了这个变量的值,其他线程可以立即能够看到这个更新。

volatile 的主要特性是:保证线程可见性和有序性,但是不保证原子性:

  1. 可见性:保证一个线程对 volatile 变量的修改,对其他线程来说是立即可见的。
  2. 有序性:禁止指令重排序。在 volatile 变量上的读写操作不会被编译器或处理器重排序,保证了代码的执行顺序与程序的顺序相同。
  3. 非原子性volatile 不能保证复合操作的原子性。比如 i++ 这样的操作,它涉及到读取-修改-写入的多步操作,volatile 不保证其原子性。

volatile 的实现原理依赖于内存屏障和缓存一致性协议(MESI)。

  • 内存屏障volatile 变量的读写会插入特定类型的内存屏障指令,来阻止重排序。对于写操作,它在写操作之后加入写屏障,保证写操作不会与其后面的操作重排序;对于读操作,在读操作之前加入读屏障,保证读操作不会与其前面的操作重排序。
  • 缓存一致性协议:每个处理器都有自己的高速缓存,当某个处理器修改了共享变量,需要缓存一致性协议来保证其他处理器也看到修改后的值。volatile 修饰的变量会被强制刷新到主内存中,而不是仅仅停留在本地缓存中,同时其他线程对这个变量的读取也会直接从主内存中读取,确保了可见性。

原理详细分析

JMM(Java 内存模型)

了解 volatile 的核心原理前,我们需要先了解下 Java 的内存模型。Java 内存模型定义了 Java 虚拟机在读写过程中对主内存(Heap)和工作内存(线程栈中的局部变量)的访问规则。

  • 主内存:所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问。
  • 工作内存:每个线程都有自己的工作内存,里面存储的是主内存中的变量副本拷贝。线程对变量的操作必须在工作内存中进行。线程首先需要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回到主内存。

Java 内存模型会带来几个问题:

  1. 可见性问题:线程 A 和线程 B 同时操作共享变量 I,线程 A 修改的结果,线程 B 是看不到的。这是因为线程 A 在工作内存中操作变量 I 的。
  2. 有序性问题:为了以提高性能,Java 内存模型是允许 JVM 和处理器对指令进行重排序的。然而,这种重排序会在并发环境下会存在一些问题。
  3. 数据不一致问题:线程 A 和线程 B 同时共享变量 I (初始值为 1)执行 + 1 操作,理想状态下,I 应该等于 3,但是在并发情况下,I 可以会等于 2。

那怎么解决呢?

volatile 是如何解决可见性问题

对于 volatile 变量,会在写入 volatile 变量的指令前添加 lock 前缀(汇编层面),当某个线程写入 volatile 变量时,其值会被强制刷入主内存,而其他处理器的缓存由于遵守了缓存一致性协议(MESI 协议),其他处理器的工作内存会被标志为无效。当其他处理器来访问这个变量时,由于它们的本地缓存是无效的,它们就不得不从主内存中重新加载这个变量的最新值。这样就保证了线程的可见性。

lock 前缀是用于实现原子操作的一种机制。当它用于一个指令前,它会锁定一个特定的内存地址,确保该指令执行期间,该内存地址不会被其他处理器访问。

MESI 协议

MESI协议,即缓存一致性协议,它是一种用于维护多处理器系统中缓存一致性的协议。从上面我们知道,每个处理器都有自己的工作内存,这可能导致同一内存位置的多个副本同时存在于不同的缓存中。为了保证这些副本的一致性,引入 MESI 协议来保证一致性。

其核心思想:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

关于 volatile 是如何解决可见性问题的,下面文章有更加详细的介绍。

什么是可见性?volatile 是如何保证可见性的?

volatile 是如何解决有序性的

为了优化程序性能编译器和处理器都可能会改变指令顺序的行为,也就说程序真正运行的顺序并不一定是我们写的代码的顺序。但是指令重排序是不会改变程序最终的运行结果。

同时,Java 内存模型允许编译器和处理器对指令进行重排序,但它必须保证在单线程环境中不改变程序的执行语义。也就是说,在单个线程中,重排序后的程序和原程序行为相同。但是,在多线程环境下,指令重排序可能会导致线程安全问题。

volatile 通过内存屏障来防止指令重排序。volatile 提供了两种内存屏障:

  • 写内存屏障:写内存屏障设置在写 volatile 变量之后。它确保对该 volatile 变量的所有写操作在任何后续对同一变量的读操作之前完成。这就意味着,在写内存屏障之前的所有普通写操作(不仅仅是对 volatile 变量的写操作)都将在写入 volatile 变量之前完成。
  • 读内存屏障:读内存屏障设置在读 volatile 变量之前。它确保对该 volatile 变量的所有读操作在任何先前的写操作之后完成。这就意味着,所有在读内存屏障之后的普通读操作(不仅仅是对 volatile 变量的读操作)都将在读取 volatile 变量之后进行。

这种内存屏障阻止了指令重排序,确保在 volatile 变量之前的操作不会被重排序到其之后,同时也确保在 volatile 变量之后的操作不会被重排序到其之前。

关于 volatile 是如何解决有序性问题的,下面文章有更加详细的介绍。

知道指令重排吗?volatile 是如何保证有序性的?

阅读全文