掘金 后端 ( ) • 2024-04-02 17:07

theme: z-blue

引言

在Java多线程编程中,volatile关键字扮演着至关重要的角色。它简单而强大,能够确保共享变量的可见性和有序性。本文将深入探讨volatile关键字的原理、使用场景以及它在Java内存模型中的作用。

什么是volatile关键字?

volatile是Java中的一个基本关键字,用于修饰变量。当一个变量被声明为volatile时,它保证每次访问变量时都会从主内存中读取,而不是从线程的工作内存中读取。这意味着,当一个线程修改了volatile变量时,其他线程能够立即看到这个改变。

JMM(Java Memory Model)java内存模型

image.png

  1. 主内存(Main Memory) : 主内存是所有线程共享的内存区域,存储了Java程序中的共享变量。线程对共享变量的读写操作都必须通过主内存来完成。
  2. 工作内存(Thread Working Memory) : 每个线程都有自己的工作内存,用于存储线程私有的数据和共享数据的副本。线程对共享变量的所有操作都必须在工作内存中进行,不能直接操作主内存中的变量。

可见性

在多线程环境中,线程通常会将变量复制到自己的工作内存中进行操作,以提高效率。但是,这种优化可能会导致一个问题:一个线程对变量的修改可能不会立即反映到其他线程的工作内存中。这就是所谓的“可见性问题”。 通过将变量声明为volatile,我们可以确保所有线程看到的变量值是一致的。这是因为volatile变量的写操作会直接作用于主内存,并且每次读取都会从主内存中获取最新的值。

有序性

volatile关键字还能防止指令重排序。在Java中,编译器和处理器可能会对指令进行重排序,以提高性能。然而,这种优化可能会导致多线程程序出现意外的行为。 当变量声明为volatile时,它会在每次写操作后插入一个写内存屏障(Store Memory Barrier),并在每次读操作前插入一个读内存屏障(Load Memory Barrier)。这些内存屏障确保了volatile变量的操作在内存中是有序的。

volatile使用场景

  1. 标志和状态:当volatile用于布尔标志或状态变量时,它可以确保线程之间的状态同步。
  2. 双重检查锁定:在实现单例模式时,volatile可以防止指令重排序,确保单例对象的正确创建。

volatile实现双重校验锁

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 私有构造函数:为了确保不能通过new关键字直接创建实例,我们将构造函数设置为私有。

  2. 静态变量instance:这是单例对象的引用,初始时设置为null

  3. volatile关键字:在instance变量前使用volatile关键字,确保了变量的读写操作对所有线程立即可见。这是双重检查锁定模式能够正常工作的关键。

  4. getInstance()方法

    • 首先,检查instance是否已经被初始化,如果已经初始化,直接返回instance
    • 如果instancenull,则进入同步块。这里的同步机制确保了只有一个线程能够进入并创建实例。
    • 在同步块内部,再次检查instance是否为null,这是因为在进入同步块的瞬间,其他线程可能已经创建了实例。这是双重检查的真正意义所在。
    • 如果instance仍然为null,则创建单例对象,并返回。

限制与替代

虽然volatile关键字非常强大,但它并不能替代所有的同步操作。volatile不能保证复合操作的原子性,例如自增操作(i++)。在这种情况下,我们需要使用锁(synchronized关键字)或原子类(如AtomicInteger)来保证操作的原子性。