JUC多并发编程 volatile

volatile关键字在Java中用于保证多线程环境下的可见性和有序性,它通过内存屏障来防止指令重排序。内存屏障确保了volatile写操作后立即刷新到主内存,以及volatile读操作时从主内存获取最新值。然而,volatile并不能保证原子性,对于需要原子性的操作,仍需借助锁或其他同步机制。文章详细解释了volatile的内存语义、内存屏障的种类及其在CPU指令层面的实现,并通过示例展示了volatile如何影响指令重排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

特点:

  • 可见性
  • 有序性(有时需要禁用重排)

内存语义:

  • 当写一个 volatile 变量时, JMM 会把该线程对应的本地内存中的共享变量值立即刷新回主存中
  • 当读一个 volatile 变量时, JMM 会把该线程对应的本地内存设置无效,重新回到主内存中读取最新共享变量
  • 所以 volatile 的写内存语义是直接刷新到主内存中,读的内存语义也是从主内存读取

如何保证可见和有序:

  • 内存屏障

内存屏障

  • 内存屏障(也称内存栅栏,屏障指令等) 是一类同步屏障指令, 是CPU或编译器对内存随机访问的操作的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。内存屏障其实就是一种 JVM 指令, Java 内存模型的重排规则会要求 Java 编译器在生成 JVM 指令时插入特定的内存屏障指令,通过这些内存屏障指令, volatile 实现了 Java 内存模型中的可见性和有序性(禁重排),但 volatile 无法保证原子性。
  • 内存屏障之前的所有写操作都要回写到主内存
  • 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
  • 因此重排序后,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话: 对一个 volatile 变量的写,先行发生于任意后续对这个 volatile 变量的读,也叫写后读

写屏障(Store Memory Barrier):

  • 告诉处理器在写屏障之前将所有存储在缓存(store buffers)中的数据同步到主内存。也就是说当看到 Store 屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行

读屏障(Load Memory Barrier):

  • 处理器在读屏障之后的读操作,都在读屏障之后执行,也就是说在 Load 屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据
  • 在读指令之前插入读屏障,让工作内存或 CPU 高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据

屏障细分四种:

// Unsafe.java
public native void loadFence();
public native void storeFence();
public native void fullFence();

// unsafe.cpp
UNSAFE_ENTRY(void, Unsafe_LoadFence(JNIEnv *env, jobject unsafe))
  UnsafeWrapper("Unsafe_LoadFence");
  OrderAccess::acquire();
UNSAFE_END

UNSAFE_ENTRY(void, Unsafe_StoreFence(JNIEnv *env, jobject unsafe))
  UnsafeWrapper("Unsafe_StoreFence");
  OrderAccess::release();
UNSAFE_END

UNSAFE_ENTRY(void, Unsafe_FullFence(JNIEnv *env, jobject unsafe))
  UnsafeWrapper("Unsafe_FullFence");
  OrderAccess::fence();
UNSAFE_END


// orderAccess.hpp
class OrderAccess : AllStatic {
 public:
  static void     loadload();
  static void     storestore();
  static void     loadstore();
  static void     storeload();

  static void     acquire();
  static void     release();
  static void     fence();

  ...
}


// orderAccess_linux_x86.inline.hpp
inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }

inline void OrderAccess::acquire() {
  volatile intptr_t local_dummy;
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}

inline void OrderAccess::release() {
  // Avoid hitting the same cache-line from
  // different threads.
  volatile jint local_dummy = 0;
}

inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}
屏障类型指令实示例说明
LoadLoadLoad1; LoadLoad; Load2保证 load1 的读取操作在 load2及后续读取操作之前执行
StoreStoreStore1; StoreStore; Store2在 store2 及其后的系操作执行前,保证 store1 的写操作已刷新到主内存
LoadStoreLoad1; LoadStore; Store2在 store2 及其后的写操作执行前,保证 load1 的读操作已经读取结束
StoreLoadStore1;StoreLoad;Load2保证 store1 的写操作已经刷新到主内存之后,load2 及其后的读操作才能执行

volatile 变量规则:

第一个操作第二个操作: 普通读写第二个操作: volatile 读第二个操作: volatile 写
普通读写可以重排可以重排不可以重排
volatile 读不可以重排不可以重排不可以重排
volatile 写可以重排不可以重排不可以重排
  • 当一个操作是 volatile 读时,不论第二个操作是什么,都不能重排序。这个操作保证了 volatile 读之后的操作不会重排到 volatile 读之前
  • 当第二个操作为volatile 写时,不论第一个操作是什么,都不会重排序。这个操作保证了 volatile 写之前的操作不会被重排到 volatile 写之后
  • 当第一个操作为 volatile 写时, 第二个操作为 volatile 读时,不能重排

volatile 读插入内存屏障后生成的指令序列示意图:

  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障, 禁止处理器把上面的 volatile 读与下面的普通读重排序。
  • 在每个 volatile 读操作后面插入一个 LoadStore 屏障,  禁止处理器把上面的 volatile 读与下面的普通写重排序

volatile 写插入内存屏障后生成的指令序列示意图:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障,可以保证在 volatile 写之前,其前面的所有普通写操作都已经刷新到主内存中
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障,作用是避免 volatile 写与后面可能有的 volatile 读/写 操作重排序

volatile 特性

保证可见性:

  • 保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见

  • read: 作用于主内存,将变量的值从主内存传输到工作内存
  • load: 作用于工作内存,将 read 从主内存传输的变量值放入工作内存变量副本中,即数据加载
  • use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当 JVM 遇到需要该变量的字节码指令时会执行该操作
  • assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当 JVM 遇到一个变量赋值字节码会执行该操作
  • store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
  • write: 作用于主内存,将 store 传输过来的变量值赋值给主内存的变量

由于 上述6条只能保证单条指令的原子性,针对多条指令的组合原子保证,没有大面积加锁,所以 JVM 提供了另外两个原子指令:

  • lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
  • unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
import java.util.concurrent.TimeUnit;

public class VolatileDemo {
    /**
     * 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
     * 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
     */
    static volatile boolean flag = true;
    // 主线程修改了 flag 之后没有将其书信到主内存,所以 t1 线程看不到
    // 主线程将 flag 刷新到了主内存,但是 t1 一直读取的是自己工作内存中的 flag 的值,没有去主内存中更新获取 flag 最新的值
    //static boolean flag = true;
    public static void main(String[] args) {
        new Thread(()-> {
            System.out.println(Thread.currentThread().getName() + "\t ----- come in");

            while(flag) {

            }

            System.out.println(Thread.currentThread().getName() + "\t ----- flag 被设置为 false, 程序停止");
        },"t1").start();

        try{ TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        flag = false;
        System.out.println(Thread.currentThread().getName() + "\t 修改完成" + flag);

    }
}

原子性:

  • 对于 volatile 变量具有可见性, JVM 只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的,但是多线程环境下,"数据计算" 和 "数据赋值" 操作可能多次出现,若数据在加载之后,若主内存 volatile 修饰变量发生修改之后,线程工作内存中的操作将会被废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见 volatile 解决的是变量读的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。
  • 不适合参加依赖当前值的运算,或者能够确保只有单一线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束

import java.util.concurrent.TimeUnit;

class MyNumber{
    int number;

    /*
    *  需要加 synchronized
    * */
    public void addPlusPlus() {
        number ++;
    }
}
public class VolatileNoAtomicDemo {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();

        for (int i = 1; i <= 10; i++) {
            new Thread(()-> {
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        try{ TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(myNumber.number);
    }
}

正确使用 volatile:

  • 单一赋值可以, but 含复合运算赋值不可以(i++ 之类)
  • 状态标志,判断业务是否结束
  • 开销较低的读,写锁策略
  • DCL 双端锁的发布

volatile 与 操作系统关联:

  •  操作系统通过 ACC_VOLATILE 标识符来按照 JMM 规范增加内存屏障

public class VolatileTest {
    volatile boolean flag;

    public VolatileTest() {
    }

    public static void main(String[] args) {
        System.out.println("------- hello world");
    }
}

  javap -v .\VolatileTest.class

{
  volatile boolean flag;
    descriptor: Z
    flags: ACC_VOLATILE

  public com.bxtech.juc.VolatileTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/bxtech/juc/VolatileTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String ------- hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值