Java volatile 作用和使用场景

volatile 是 Java 中的一个关键字,用于修饰变量。它是 Java 内存模型 (JMM) 中实现可见性有序性(禁止指令重排序)的一种轻量级同步机制。
一、volatile 的作用

volatile 关键字有两个主要作用:

  1. 保证可见性 (Visibility):

    • 什么是可见性问题?

      • 在多线程环境下,每个线程都有自己的工作内存(例如 CPU 缓存)。
      • 线程对共享变量的修改,首先会在自己的工作内存中进行,然后才会被刷新到主内存中。
      • 其他线程可能无法立即看到这个修改,因为它们可能还在读取自己工作内存中的旧值。
      • 这就是可见性问题:一个线程对共享变量的修改,对其他线程不可见。
    • volatile 如何保证可见性?

      • 当一个变量被声明为 volatile 时,Java 内存模型 (JMM) 会确保:
        • 写操作: 当一个线程修改了 volatile 变量的值,这个新值会立即被刷新到主内存中。
        • 读操作: 当一个线程读取 volatile 变量的值,它会强制从主内存中读取最新值,而不是从自己的工作内存中读取。
    • 底层原理 (简化):

      • 写操作: 在写 volatile 变量时,会插入一个 StoreStore 屏障 和一个 StoreLoad 屏障
        • StoreStore 屏障:禁止上面的普通写和下面的 volatile 写重排序。
        • StoreLoad 屏障:强制刷新处理器缓存,确保其他处理器能够看到 volatile 变量的最新值。
      • 读操作: 在读 volatile 变量时,会插入一个 LoadLoad 屏障 和一个 LoadStore 屏障
        • LoadLoad 屏障:禁止上面的 volatile 读和下面的普通读重排序。
        • LoadStore 屏障:禁止上面的 volatile 读和下面的普通写重排序。
        • 还会强制从主内存读取 volatile 变量的值。
  2. 禁止指令重排序 (Ordering):

    • 什么是指令重排序?

      • 为了优化性能,编译器和处理器可能会对指令的执行顺序进行重新排序,只要不影响单线程程序的执行结果。
      • 但在多线程环境下,指令重排序可能会导致意想不到的问题。
    • volatile 如何禁止指令重排序?

      • volatile 关键字可以禁止特定类型的指令重排序,具体规则如下:
        • 写操作:
          • 不能将 volatile 写操作重排序到之前的任何操作之前。
          • 不能将 volatile 写操作重排序到之后的其他 volatile 写操作之后。
        • 读操作:
          • 不能将 volatile 读操作重排序到之后的任何操作之后。
          • 不能将 volatile 读操作重排序到之前的其他 volatile 读操作之前。
    • 内存屏障 (Memory Barrier):

      • volatile 的禁止指令重排序是通过 内存屏障 (Memory Barrier) 来实现的。
      • 内存屏障是一种特殊的 CPU 指令,可以强制处理器按照特定的顺序执行内存操作,并刷新处理器缓存。

二、volatile 的使用场景

volatile 适用于以下场景:

  1. 状态标志 (Status Flag):

    • 场景描述:

      • 使用一个 boolean 类型的变量作为状态标志,控制线程的执行。
      • 例如,一个线程检查标志位,决定是否继续执行;另一个线程修改标志位,通知其他线程停止执行。
    • volatile 的作用:

      • 保证标志位的可见性,确保线程能够及时看到标志位的变化。
    • 代码示例:

      public class VolatileFlagExample {
      
          private volatile boolean running = true; // 使用 volatile 修饰
      
          public void start() {
              new Thread(() -> {
                  while (running) {
                      // 执行任务
                      System.out.println("Running...");
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
                  System.out.println("Stopped.");
              }).start();
          }
      
          public void stop() {
              running = false; // 修改标志位
          }
      
          public static void main(String[] args) throws InterruptedException {
              VolatileFlagExample example = new VolatileFlagExample();
              example.start();
              Thread.sleep(2000);
              example.stop();
          }
      }
      
  2. 双重检查锁定 (Double-Checked Locking) 单例模式 (JDK 1.5 之后):

    • 场景描述:

      • 单例模式的一种实现方式,延迟初始化,只有在第一次使用时才创建实例。
      • 为了保证线程安全,需要进行同步处理。
      • 双重检查锁定是一种优化手段,可以减少同步的开销。
    • volatile 的作用:

      • 禁止指令重排序,确保 instance = new Singleton() 这条语句的原子性.
        这条语句并非原子操作,实际上包含三个步骤:
      1. 分配内存空间
      2. 初始化对象
      3. 将 instance 指向分配的内存空间。
        如果没有 volatile,可能会发生指令重排序,导致一个线程获取到未完全初始化的对象。
    • JDK1.5 之前,即使是使用了双重检查,由于volatile 关键字语义不完善, DCL仍然不能保证安全。

    • 代码示例:

      public class Singleton {
      
          private volatile static Singleton instance; // 使用 volatile 修饰
      
          private Singleton() {}
      
          public static Singleton getInstance() {
              if (instance == null) { // 第一次检查
                  synchronized (Singleton.class) { // 加锁
                      if (instance == null) { // 第二次检查
                          instance = new Singleton();
                      }
                  }
              }
              return instance;
          }
      }
      
  3. 发布-订阅模式中的可见性:

    • 场景描述:

      • 一个线程发布(修改)一个共享变量,其他线程订阅(读取)这个变量的变化。
    • volatile 的作用:

      • 保证发布线程对共享变量的修改对订阅线程可见。
  4. 写多读少的场景:

    • 如果一个变量被多个线程读取, 但只有一个线程写入,使用 volatile 可以提供轻量级的同步。

三、volatile 的局限性 (不能做什么)

  • volatile 不能保证原子性:

    • volatile 只能保证可见性和有序性,不能保证原子性。
    • 对于复合操作(例如 i++),volatile 无法保证线程安全。 i++ 操作实际上包含三个步骤:读取 i 的值、将 i 的值加 1、将新值写回 i。 即使 i 是 volatile 的,这三个步骤也不是原子的,多个线程同时执行 i++ 仍然可能导致错误。
  • volatile 不能代替锁:

    • volatile 是一种轻量级的同步机制,它比锁的开销要小。
    • volatile 的功能有限,只能用于特定场景,不能完全代替锁。
    • 如果需要保证复合操作的原子性,或者需要更复杂的同步控制,仍然需要使用锁(例如 synchronizedReentrantLock)或原子类。

四、volatilesynchronized 的比较

特性volatilesynchronized
作用保证可见性、禁止指令重排序保证原子性、可见性、有序性
使用方式修饰变量修饰方法或代码块
性能轻量级,开销较小重量级,开销较大
阻塞不阻塞线程阻塞线程
死锁不会导致死锁可能导致死锁
原子性不保证原子性 (只保证单个 volatile 变量的读写操作的原子性)保证原子性 (被 synchronized 修饰的代码块或方法具有原子性)
使用场景状态标志、双重检查锁定单例模式、发布-订阅模式需要保证原子性、可见性和有序性的场景,例如对多个变量进行复合操作、实现复杂的同步逻辑
与 CAS 的关系CAS 操作通常需要 volatile 变量来保证可见性和有序性。 CAS 提供了原子性的比较并交换操作, 结合 volatile 变量的可见性, 就可以实现一些无锁的算法。synchronized 内部使用了监视器锁 (Monitor Lock) 来实现同步。 Java 对象头中的 Mark Word 用于存储锁的信息。 JDK 1.6 之后,synchronized 进行了优化,引入了偏向锁、轻量级锁、重量级锁等机制, 其中轻量级锁的实现就利用了 CAS。
实现JVM通过插入内存屏障实现JVM 基于进入和退出 Monitor Object 来实现方法同步和代码块同步, monitorenter 和 monitorexit. 代码块同步是使用 monitorentermonitorexit 指令实现的,方法同步使用另一种方式实现(通常是添加 ACC_SYNCHRONIZED 标志)

总结:

  • volatile 是 Java 中的一个关键字,用于修饰变量,提供轻量级的同步机制。
  • volatile 的主要作用是保证可见性和禁止指令重排序,但不能保证原子性。
  • volatile 适用于状态标志、双重检查锁定单例模式、发布-订阅模式等场景。
  • volatile 不能代替锁,在需要保证复合操作的原子性或更复杂的同步控制时,仍然需要使用锁或原子类。
  • 理解 volatile 的原理和使用场景,可以帮助你编写正确的并发程序,避免可见性和有序性导致的问题。

五、 深入理解 volatile (高级主题)

  1. happens-before 关系:

    • Java 内存模型 (JMM) 定义了 happens-before 关系,用于描述操作之间的可见性。
    • 如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,并且 A 的执行顺序在 B 之前。
    • volatile 变量的写操作 happens-before 后续对该变量的读操作。
    • 这意味着,如果一个线程写入一个 volatile 变量,那么后续任何线程读取这个 volatile 变量,都能看到之前写入的值。
  2. 内存屏障 (Memory Barrier) 的细节:

    • 内存屏障是一种 CPU 指令,用于控制内存访问的顺序和可见性。
    • 不同的 CPU 架构有不同的内存屏障指令。
    • Java 内存模型 (JMM) 定义了一组抽象的内存屏障,JVM 会根据具体的 CPU 架构将这些抽象的内存屏障映射到具体的 CPU 指令。
    • volatile 关键字的实现依赖于内存屏障:
      • StoreStore 屏障: 禁止上面的普通写和下面的 volatile 写重排序。
      • StoreLoad 屏障: 强制刷新处理器缓存,确保其他处理器能够看到 volatile 变量的最新值。
      • LoadLoad 屏障: 禁止上面的 volatile 读和下面的普通读重排序。
      • LoadStore 屏障: 禁止上面的 volatile 读和下面的普通写重排序。
      • 更详细地说:
        • 在每个volatile写操作前插入StoreStore屏障。
        • 在每个volatile写操作后插入StoreLoad屏障。
        • 在每个volatile读操作后插入LoadLoad屏障。
        • 在每个volatile读操作后插入LoadStore屏障。
  3. volatile 与缓存一致性协议 (Cache Coherence Protocol):

    • 在多核处理器中,每个 CPU 核心都有自己的缓存(L1, L2, L3 缓存)。
    • 为了保证多个 CPU 核心之间的缓存数据一致性,需要使用缓存一致性协议(例如 MESI 协议)。
    • volatile 变量的写操作会触发缓存一致性协议,将新值写入主内存,并使其他 CPU 核心中该变量的缓存失效。
  4. volatile 与原子类的关系:
    原子类内部使用了volatile 和 CAS操作来实现原子性、可见性和有序性。 原子类中的变量通常会被声明为 volatile

六、 最佳实践

  1. 明确 volatile 的适用场景: 不要滥用 volatile,只有在确实需要保证可见性和禁止指令重排序时才使用它。
  2. 理解 volatile 的局限性: volatile 不能保证原子性,不能代替锁。
  3. 优先使用原子类: 如果需要原子性操作,优先使用 Java 并发包提供的原子类 (例如 AtomicInteger, AtomicLong, AtomicReference 等),而不是直接使用 volatile 和 CAS。
  4. 谨慎使用双重检查锁定: 只有在 JDK 1.5 及以上版本,并且正确使用 volatile 关键字的情况下,双重检查锁定才是安全的。
  5. 结合具体场景分析: 对于复杂的并发场景,要仔细分析,确定是否需要使用 volatile,以及如何正确使用。

**总结 **

volatile 是 Java 并发编程中一个重要的关键字,它可以提供轻量级的同步机制,保证可见性和禁止指令重排序。 理解 volatile 的原理、作用、使用场景和局限性,是编写正确的并发程序的基础。 在实际开发中,要根据具体情况,合理使用 volatile、原子类、锁等同步机制,构建高效、可靠的多线程应用程序。 尽量避免直接使用底层的Unsafe类进行CAS操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰糖心书房

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值