volatile最全详解(万字图文)

volatile最全详解(万字图文)-mikechen

volatile是Java多线程并发编程非常重要的知识点,也是面试经常考的内容,下面我就全面来详解volatile@mikechen

volatile定义

volatile 是 Java 中的一个关键字,用于修饰变量,用以确保多线程并发访问变量时的可见性和有序性。

 

volatile作用

volatile关键字的作用:主要用于以下两个方面:保证可见性与有序性。

1.保证可见性

volatile 保证了当一个线程修改一个 volatile 变量的值时,其他线程能够立即看到这个变量的修改。

这是因为它会强制将变量的值从工作内存刷新到主内存,以便其他线程可以读取到最新的值。

 

2.保证有序性

volatile 保证了被修饰的变量的读写操作是按照一定的顺序发生的,不会被重排序。

 

Volatile可见性

在多线程编程中,一个线程对共享变量的修改不一定会立即对其他线程可见。

这是因为线程可以将变量缓存在自己的工作内存中,而不是直接从主内存中读取或写入。

这会导致以下问题:

  • 线程A写入一个变量的新值到工作内存。
  • 线程B读取相同的变量。
  • 线程B从自己的工作内存中读取变量的旧值,而不是线程A修改后的新值。

使用Volatile关键字可以避免这种问题的发生,因为它会告诉编译器和CPU,该变量的值可能随时会被其他线程修改。

如下图所示:

volatile最全详解(万字图文)-mikechen

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 关键字的一个重要作用是防止指令重排,指令重排是编译器和处理器为了优化性能而进行的一种优化技术。

在多线程环境下,指令重排可能导致问题,因为它可能会改变多线程程序中的执行顺序,导致不正确的结果。

例如:如果一个线程在另一个线程之前修改了一个共享变量,但指令重排导致修改在另一个线程之后执行,那么就会出现问题。

如下图所示:
volatile最全详解(万字图文)-mikechen

比如: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最全详解(万字图文)-mikechen

当一个线程对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面试题总结

评论交流
    说说你的看法