volatile关键字
发布日期:2021-05-06 23:32:04 浏览次数:20 分类:精选文章

本文共 2905 字,大约阅读时间需要 9 分钟。

前言

在Java中,如果为了读写一个或两个实例域使用同步,内存开销会变得非常大。volatile关键字正是为实例域的同步访问提供了一个免锁机制。如果一个域被声明为volatile,编译器和虚拟机会了解该域可能会被另一线程并发修改。在此之前,我们需要了解Java内存模型及其并发编程的三大特性:原子性、可见性和有序性。

一、内存模型

对象实例存储在堆内存中,而堆内存是所有线程共享的运行时内存区域。因此,共享变量的可见性问题变得重要。与此同时,局部变量和方法参数等并不共享内存,自然也不受内存模型的影响。线程之间共享的变量都存储在主存中,而每个线程有自己的本地内存(工作内存),存储该线程共享变量的副本。需要注意的是,本地内存只是Java内存模型的一个抽象概念,并不真实存在,包含了缓存、写缓冲区和寄存器等区域。Java内存模型负责控制线程间的通信,决定一个线程对主存中共享变量的写入对其他线程何时可见。

如果线程A和线程B需要通信,必须经历以下步骤:

  • 线程A需要将本地内存中更新过的共享变量刷新到主存中。
  • 线程B再从主存中获取更新后的共享变量。
  • 例如,如果有两个线程同时执行自增操作,线程A读取i的值到本地内存,线程B也读取i的值到本地内存。此时i的值可能都为10。线程A执行+1并将结果写入主存,线程B依然读取到10的值并执行+1,最终结果为11而非12,这就是缓存一致性问题。这种被多个线程访问的变量称为共享变量。

    二、原子性

    对基本数据类型变量的读取、赋值操作是原子性操作,即不可被中断。例如:

    x = 3; // 语句1
    y = x; // 语句2
    x++; // 语句3

    语句1是原子性操作,而语句2和3都不是。只有简单的读取和赋值操作是原子性的。如果一个语句包含多个操作,就不是原子性操作。例如,x++包含读取x的值、加1并写入工作内存这三个操作,因此不是原子性操作。为了保证原子性操作,可以使用java.util.concurrent.atomic包中的类,如AtomicInteger中的incrementAndGetdecrementAndGet方法。

    三、可见性

    可见性指的是线程间的可见性。修改后的状态必须对其他线程可见。使用volatile修饰共享变量,可以保证修改后的值立即被写入主存,使其对其他线程可见。普通的共享变量不能保证可见性,因为它们可能不会被立即写入主存,也不确定何时写入主存。例如,线程B可能读取到旧值而无法立即看到线程A的修改。

    四、有序性

    Java内存模型允许编译器和处理器对指令进行重排序。虽然重排序不会影响单线程的正确性,但会影响多线程的并发执行。volatile关键字可以禁止指令重排序,确保指令执行的有序性。例如,使用volatile修饰的变量操作前面的指令必须在所有操作执行完毕后才会执行,后面的指令也不会被排在volatile操作前面。

    五、深入理解volatile关键字

  • volatile保证可见性

    • 当共享变量被volatile修饰后,修改后的值对其他线程立即可见。
    • volatile还禁止指令重排序,防止重排序带来的内存不一致问题。
  • volatile不保证原子性

    • 例如:
    public class VolatileTest {
    public volatile int inc = 0;
    public void increase() {
    inc++;
    }
    public static void main(String[] args) {
    final VolatileTest test = new VolatileTest();
    for (int i = 0; i < 10; i++) {
    new Thread() {
    @Override
    public void run() {
    for (int j = 0; j < 1000; j++) {
    test.increase();
    }
    }
    }.start();
    }
    while (Thread.activeCount() > 2) {
    Thread.yield();
    }
    System.out.println("结果:" + test.inc);
    }
    }

    该代码的结果每次运行都不相同,因为自增操作不具备原子性。inc++包含读取原始值、加1并写入工作内存的三个操作。线程A和线程B可能会同时读取inc的值并进行多次加1操作,导致最终结果不正确。

  • volatile保证有序性

    volatile关键字禁止指令重排序,确保指令执行的有序性。Lock前缀指令(内存屏障)确保在volatile变量操作前,所有指令已完成,且后续指令不会被排在volatile操作前面。

  • 六、volatile的实现原理

  • 可见性

    处理器为了提高性能,通常不会直接与主存通信,而是通过缓存中间层。volatile变量的写操作会向处理器发送Lock前缀指令,将缓存行数据强制写入主存,确保其他线程可以立即读取到最新值。然而,其他处理器的缓存可能仍然存有旧值。

  • 有序性

    Lock前缀指令作为内存屏障,确保指令重排序不会打乱执行顺序。volatile变量的操作前,所有指令必须先执行完毕,后续指令也不会被排在volatile操作前面。

  • 七、volatile应用场景

  • 具备条件

    • 写操作不依赖于当前值(如自增、自减)。
    • 变量未包含在具有其他变量的不变式中。
  • 使用场景举例

    • 状态标记

      volatile boolean flag = false;
      • 单例模式中的双重检查模式(DCL)
      public class Singleton {
      private volatile static Singleton instance = null;
      public static Singleton getInstance() {
      if (instance == null) {
      synchronized (this) {
      if (instance == null) {
      instance = new Singleton();
      }
      }
      }
      return instance;
      }
      }

      使用volatile修饰instance变量是为了防止多线程环境下由于指令重排序导致的单例对象初始化问题。

  • 上一篇:CAS原子操作
    下一篇:生产者、消费者模式实现

    发表评论

    最新留言

    感谢大佬
    [***.8.128.20]2025年04月06日 09时49分03秒

    关于作者

        喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
    -- 愿君每日到此一游!

    推荐文章