回答
volatile
是 Java 提供的一种轻量级的同步机制,与 synchronized 修饰方法、代码块不同,volatile
只能用来修饰变量。当一个变量被声明为 volatile
后,它会确保所有线程看到该变量的值都是一致的,即一个线程更新了这个变量的值,其他线程可以立即能够看到这个更新。
volatile 的主要特性是:保证线程可见性和有序性,但是不保证原子性:
- 可见性:保证一个线程对
volatile
变量的修改,对其他线程来说是立即可见的。 - 有序性:禁止指令重排序。在
volatile
变量上的读写操作不会被编译器或处理器重排序,保证了代码的执行顺序与程序的顺序相同。 - 非原子性:
volatile
不能保证复合操作的原子性。比如i++
这样的操作,它涉及到读取-修改-写入的多步操作,volatile
不保证其原子性。
volatile
的实现原理依赖于内存屏障和缓存一致性协议(MESI)。
- 内存屏障:
volatile
变量的读写会插入特定类型的内存屏障指令,来阻止重排序。对于写操作,它在写操作之后加入写屏障,保证写操作不会与其后面的操作重排序;对于读操作,在读操作之前加入读屏障,保证读操作不会与其前面的操作重排序。 - 缓存一致性协议:每个处理器都有自己的高速缓存,当某个处理器修改了共享变量,需要缓存一致性协议来保证其他处理器也看到修改后的值。
volatile
修饰的变量会被强制刷新到主内存中,而不是仅仅停留在本地缓存中,同时其他线程对这个变量的读取也会直接从主内存中读取,确保了可见性。
原理详细分析
JMM(Java 内存模型)
了解 volatile
的核心原理前,我们需要先了解下 Java 的内存模型。Java 内存模型定义了 Java 虚拟机在读写过程中对主内存(Heap)和工作内存(线程栈中的局部变量)的访问规则。
- 主内存:所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问。
- 工作内存:每个线程都有自己的工作内存,里面存储的是主内存中的变量副本拷贝。线程对变量的操作必须在工作内存中进行。线程首先需要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回到主内存。
Java 内存模型会带来几个问题:
- 可见性问题:线程 A 和线程 B 同时操作共享变量 I,线程 A 修改的结果,线程 B 是看不到的。这是因为线程 A 在工作内存中操作变量 I 的。
- 有序性问题:为了以提高性能,Java 内存模型是允许 JVM 和处理器对指令进行重排序的。然而,这种重排序会在并发环境下会存在一些问题。
- 数据不一致问题:线程 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 是如何解决有序性的
为了优化程序性能编译器和处理器都可能会改变指令顺序的行为,也就说程序真正运行的顺序并不一定是我们写的代码的顺序。但是指令重排序是不会改变程序最终的运行结果。
同时,Java 内存模型允许编译器和处理器对指令进行重排序,但它必须保证在单线程环境中不改变程序的执行语义。也就是说,在单个线程中,重排序后的程序和原程序行为相同。但是,在多线程环境下,指令重排序可能会导致线程安全问题。
volatile
通过内存屏障来防止指令重排序。volatile
提供了两种内存屏障:
- 写内存屏障:写内存屏障设置在写
volatile
变量之后。它确保对该volatile
变量的所有写操作在任何后续对同一变量的读操作之前完成。这就意味着,在写内存屏障之前的所有普通写操作(不仅仅是对volatile
变量的写操作)都将在写入volatile
变量之前完成。 - 读内存屏障:读内存屏障设置在读
volatile
变量之前。它确保对该volatile
变量的所有读操作在任何先前的写操作之后完成。这就意味着,所有在读内存屏障之后的普通读操作(不仅仅是对volatile
变量的读操作)都将在读取volatile
变量之后进行。
这种内存屏障阻止了指令重排序,确保在 volatile
变量之前的操作不会被重排序到其之后,同时也确保在 volatile
变量之后的操作不会被重排序到其之前。
关于 volatile 是如何解决有序性问题的,下面文章有更加详细的介绍。