volatile是Java多线程并发编程非常重要的知识点,也是面试经常考的内容,下面我就全面来详解volatile@mikechen
volatile定义
volatile 是 Java 中的一个关键字,用于修饰变量,用以确保多线程并发访问变量时的可见性和有序性。
volatile作用
volatile关键字的作用:主要用于以下两个方面:保证可见性与有序性。
1.保证可见性
volatile 保证了当一个线程修改一个 volatile 变量的值时,其他线程能够立即看到这个变量的修改。
这是因为它会强制将变量的值从工作内存刷新到主内存,以便其他线程可以读取到最新的值。
2.保证有序性
volatile 保证了被修饰的变量的读写操作是按照一定的顺序发生的,不会被重排序。
Volatile可见性
在多线程编程中,一个线程对共享变量的修改不一定会立即对其他线程可见。
这是因为线程可以将变量缓存在自己的工作内存中,而不是直接从主内存中读取或写入。
这会导致以下问题:
- 线程A写入一个变量的新值到工作内存。
- 线程B读取相同的变量。
- 线程B从自己的工作内存中读取变量的旧值,而不是线程A修改后的新值。
使用Volatile关键字可以避免这种问题的发生,因为它会告诉编译器和CPU,该变量的值可能随时会被其他线程修改。
如下图所示:
Volatile可以确保在多线程环境下变量的可见性,每次访问该变量时都需要从内存中读取最新的值。
也就是说当一个线程修改了Volatile变量的值时,其他线程可以立即看到这个修改后的值,而不是使用之前缓存的值。
如下所示:
public class VolatileVisibilityExample { private volatile boolean flag = false; public void toggleFlag() { flag = !flag; } public boolean isFlag() { return flag; } public static void main(String[] args) { VolatileVisibilityExample example = new VolatileVisibilityExample(); Thread writerThread = new Thread(() -> { example.toggleFlag(); System.out.println("Flag is set to true"); }); Thread readerThread = new Thread(() -> { while (!example.isFlag()) { // Busy-wait until flag becomes true } System.out.println("Flag is now true"); }); writerThread.start(); readerThread.start(); } }
在上述示例中,volatile修饰的flag变量用于在一个线程中设置为true,并在另一个线程中等待其变为true。
由于使用了volatile,readerThread能够立即看到flag的变化,而不需要额外的同步手段。
Volatile指令重排
volatile 关键字的一个重要作用是防止指令重排,指令重排是编译器和处理器为了优化性能而进行的一种优化技术。
在多线程环境下,指令重排可能导致问题,因为它可能会改变多线程程序中的执行顺序,导致不正确的结果。
例如:如果一个线程在另一个线程之前修改了一个共享变量,但指令重排导致修改在另一个线程之后执行,那么就会出现问题。
如下图所示:
比如:singleton = new Singleton()分配内存的语句。
就会出现上图的3个步骤:
- 1. 给 singleton 分配内存。
- 2. 调用 Singleton 的构造函数,来初始化成员变量,形成实例。
- 3. 将singleton对象指向分配的内存空间。
但是在某些情况下,这种重排可能会导致程序出现意外的结果,特别是在多线程环境下,就会出现上图右侧的顺序的问题。
使用Volatile关键字可以告诉编译器和CPU,该变量的值可能随时被其他线程修改,因此不能对该变量的读取和写入进行重排。
以下是一个示例,演示了volatile的防止指令重排的作用:
public class VolatileReorderingExample { private volatile int x = 0; private volatile int y = 0; private volatile boolean flag = false; public void writer() { x = 1; // 第1步 y = 2; // 第2步 flag = true; // 第3步 } public void reader() { if (flag) { int result = y * y; // 第4步 System.out.println("Result: " + result); } int value = x; // 第5步 System.out.println("Value: " + value); } }
在上述示例中,如果没有volatile关键字,编译器和处理器可能会重排writer方法的指令,导致在reader方法中的第5步在第3步之前执行。
这将导致value的值为0,而不是1。
但是,使用volatile关键字确保了写-读屏障,阻止了这种重排序,保证了value的值为1。
总之,volatile关键字不仅确保了变量的可见性,还防止了编译器和处理器进行不合理的指令重排,从而维护了多线程程序的正确性。
volatile的实现原理
volatile的实现原理涉及:内存屏障、主内存、读写屏障和缓存一致性协议等底层机制,以确保变量的可见性和有序性。
1.内存屏障
volatile 使用内存屏障,也称为内存栅栏或内存栅障,来阻止指令重排,以确保变量的写操作不会被重排序到变量的读操作之前。
内存屏障是一种硬件层面的机制,确保对共享变量的操作具有一定的顺序性。
2.主内存
在 Java 内存模型中,所有线程都共享一个主内存(Main Memory),而每个线程都有自己的工作内存(Working Memory)。
如下图所示:
当一个线程对volatile变量进行写操作时,它会将变量的值写入主内存,并强制刷新(flush)所有之前的写操作到主内存。
3.读写屏障
volatile 使用读-写屏障来确保对volatile变量的读操作会从主内存中获取最新的值,而不会使用工作内存中的值。
这也意味着在读操作之前的所有写操作都必须被刷新到主内存。
4.禁止指令重排
volatile 禁止编译器和处理器对volatile变量的读写操作进行指令重排。这确保了变量的写操作不会被重排序到它的读操作之前。
5.在多核处理器上的实现
在多核处理器上,volatile 变量的写操作会导致缓存一致性协议。
例如:MESI协议的刷新,以确保其他核心能够看到最新的值。
volatile的使用场景
1.标志位
volatile
常用于标志位,用于控制线程的启动、停止或状态切换。
当一个线程设置一个 volatile
标志位时,其他线程可以及时看到状态的改变,从而协调线程的行为。
2.双重检查锁定
volatile 可以用于确保单例模式的实现在多线程环境下只创建一个实例,这是一种延迟初始化的线程安全方式。
3.轻量级同步
volatile 通常比使用锁来实现的同步更轻量级,适用于一些简单的并发场景,例如:用于确保计数器的原子性操作。
4.避免指令重排
volatile 变量可以防止编译器和处理器对变量的读写操作进行指令重排,这对于一些需要特定执行顺序的场景非常重要。
总的来说,volatile 是一种用于确保变量的可见性和有序性的简单而有用的机制。
mikechen
mikechen睿哥,10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!

后台回复【架构】即可获取《阿里架构师进阶专题全部合集》,后台回复【面试】即可获取《史上最全阿里Java面试题总结》