Tao
Tao

volatile使用指南

相信大家对volatile都很熟悉,在jdk1.4之前volatile语义只能保证可见性,不能保证被有序性,但是在jdk1.5版本后,jsr-133 java内存模型规范实行后,volatile语义被增强,支持可见性和有序性(禁止指令重排)。本文将介绍volatile语义、使用场景及最佳实践。

在Java中,volatile是一个关键字,用于确保内存的可见性并避免在多线程应用程序中重新排序问题。当一个变量被声明为volatile时,Java 线程每次读取变量时都会读取其最新写入的值。这就是说,它可以用来确保多个线程能够正确地共享变量。 volatile关键字主要用于以下两种情况:

  • 内存可见性:确保所有线程看到的共享变量值都是一致的。
  • 防止指令重排序:确保不会因为编译器的优化而导致代码的执行顺序与预期不符。

jdk1.5版本后,volatile可以保证可见性和有序性。

  • 可见性(Visibility):当一个线程修改了一个被 volatile 修饰的变量的值时,该变量的新值将立即对其他线程可见。这是由于在写入 volatile 变量时,会强制将修改后的值立即刷新到主内存,并使其他线程能够看到最新的值。类似地,当一个线程读取一个被 volatile 修饰的变量时,它会从主内存中获取最新的值,而不是使用线程的本地缓存。
  • 有序性(禁止指令重排序Preventing Instruction Reordering):使用 volatile 关键字修饰的变量的读写操作会被插入内存屏障(Memory Barrier),这样可以防止指令重排序。指令重排序是编译器和处理器为了优化程序执行而进行的一种优化技术,它可以改变指令的执行顺序,但有时可能会导致多线程环境下的问题。通过使用 volatile 关键字,可以确保在写入 volatile 变量之前的操作不会被重排序到写入之后,而在读取 volatile 变量之后的操作不会被重排序到读取之前。

由于volatile 无法保证原子性,不能用于非原子操作,比如i++。 适应场景:

  • 轻量级同步需求:volatile 关键字提供了一种轻量级的同步机制,适用于一些简单的并发场景。例如,使用 volatile 变量进行简单的计数器操作。
  • 标志位场景 如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。具体一个简单demo:

VolatileDemo.java

public class VolatileDemo {
  private volatile boolean flag = false;
  public void start() {
      new Thread(() -> {
          System.out.println("Thread 1: Waiting for flag to be true...");
          while (!flag) {
          }
          System.out.println("Thread 1: Flag is now true. Exiting...");
      }).start();
      new Thread(() -> {
          System.out.println("Thread 2: Sleeping for 2 seconds...");
          try {
              Thread.sleep(2000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("Thread 2: Setting flag to true...");
          flag = true;
      }).start();
  }
  public static void main(String[] args) {
      VolatileDemo demo = new VolatileDemo();
      demo.start();
  }
  }
  • 作为刷新之前变量的触发器 用了volatile int x后,可以保证读取x后,之前的所有变量都可见。这个主要基于happen-before原则来实现的。具体提供一个demo:

VolatileTriggerDemo.java

package com.bearboy.thread.jmm;
import java.util.concurrent.TimeUnit;
public class VolatileTriggerDemo {
    private volatile int x = 0;
    private boolean flag = false;
    public void writerThread() {
        try {
            Thread.sleep(1000);
            flag = true;
            x = 1;  // 修改 x 的值
            System.out.println("set x=1 end....");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    public void readerThread() {
        int j = 0;
        while (!flag) {
            j = x;
            System.out.println("j = " +j);
        }
        System.out.println("end Value of x: " + x);
    }
    public static void main(String[] args) {
        VolatileTriggerDemo demo = new VolatileTriggerDemo();
        Thread writerThread = new Thread(demo::writerThread);
        Thread readerThread = new Thread(demo::readerThread);
        writerThread.start();
        readerThread.start();
    }
}
  • 可见性保证:volatile 关键字可以确保对变量的写操作对其他线程可见,因此,应该将 volatile 用于需要在多个线程之间共享状态的变量。这样可以避免线程之间的数据不一致性问题。
  • 避免复合操作:volatile 关键字不能保证复合操作的原子性,如果需要进行复合操作(例如递增或递减),应该考虑使用原子类(如 AtomicInteger)或使用其他同步机制(如 synchronized 或 Lock)来确保原子性。
  • 避免过度使用:volatile 关键字会增加一定的开销,因此,应该避免过度使用。只有在需要保证可见性的场景下使用 volatile,而不是将其应用于所有变量。
  • 避免依赖原子性:volatile 关键字不能保证原子性,因此,在需要原子性操作的情况下,不应该依赖 volatile。应该使用原子类(如 AtomicInteger、AtomicLong 等)或其他同步机制来确保原子性。
  • 配合其他同步机制使用:volatile 关键字适用于一些简单的同步需求,但对于更复杂的同步需求,应该配合使用其他同步机制,如 synchronized 关键字、Lock 接口等,以提供更强大的线程安全保证。
  • 正确理解内存语义:使用 volatile 关键字的同时,需要正确理解其提供的内存语义。volatile 关键字保证了可见性和禁止指令重排序,但并不提供原子性。
  • 考虑使用并发集合类:Java 提供了许多并发集合类,如 ConcurrentHashMap、ConcurrentLinkedQueue 等,它们内部使用了适当的同步机制来保证线程安全性。在多线程环境下,应该优先考虑使用这些并发集合类,而不是手动使用 volatile。

总而言之,volatile 关键字是一种轻量级的同步机制,适用于一些简单的并发场景,需要注意它的使用限制和内存语义,并结合其他同步机制来提供更强大的线程安全保证。在设计和实现多线程应用程序时,需要根据具体需求综合考虑使用 volatile 和其他同步机制的合适性。

相关内容