1. 被 volatile 变量修饰后
特点:有序性 和 可见性
volatile 的内存语义
写一个 volatile 变量时, JMM 会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
所以 volatile 的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
2. 内存屏障
2.1 是什么
内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
内存屏障其实就是一种 JVM 指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile 实现了 Java内存模型中的可见性和有序性,但volatile 无法保证原子性。
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。
一句话:对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。
2.2 volatile 凭什么可以保证可见性和有序性
直接读写主内存 + 内存屏障强制刷新
内存屏障禁止指令重排序
2.3 JVM 提供了四种内存屏障指令
在每一个 volatile 写操作前面插入一个 StoreStore 内存屏障
在每一个 volatile 写操作后面插入一个 StoreLoad 内存屏障
在每一个 volatile 读操作前面插入一个 LoadLoad 内存屏障
在每一个 volatile 读操作后面插入一个 LoadStore 内存屏障
3. volatile 特性
3.1 可见性
保证不同线程对这个变量进行操作时的可见性,即变量一旦改变,所有线程立即可见。
代码演示:
publicclassVolatileSeeDemo{staticbooleanflag=true;//不加volatile,没有可见性//static volatile boolean flag = true; //加了volatile,保证可见性publicstaticvoidmain(String[]args){newThread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");while(flag){}System.out.println(Thread.currentThread().getName()+"\t flag被修改为false,退出.....");},"t1").start();//暂停2秒钟后让main线程修改flag值try{TimeUnit.SECONDS.sleep(2);}catch(InterruptedExceptione){e.printStackTrace();}flag=false;System.out.println("main线程修改完成");}}解决:
使用 volatile 修饰共享变量,就可以达到上面的效果,被 volatile 修改的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
3.2 有序性
volatile 关键字通过内存屏障(Memory Barrier)和happens-before规则来保证有序性,具体实现如下:
- 编译器和处理器会对指令进行重排序以优化性能,但在多线程环境下可能导致不可预期的结果。volatile 变量的读写操作会插入内存屏障,禁止特定类型的重排序:
- 写操作(Store):
- 在 volatile 写操作前插入 StoreStore 屏障,确保所有普通写操作(非 volatile)在此之前完成。
- 在 volatile 写操作后插入 StoreLoad 屏障,防止 volatile 写与后续的 volatile 读/写重排序。
- 读操作(Load):
- 在 volatile 读操作后插入 LoadLoad 屏障和 LoadStore 屏障,确保后续操作不会重排到 volatile 读之前。
- happen-before 规则
根据 JMM(Java 内存模型)的 happens-before 原则:
- 写操作先行于读操作:对 volatile 变量的写操作 happens-before 后续所有对该变量的读操作。 - 编译器/处理器必须遵守此规则,不会破坏这种顺序性。volatile 通过内存屏障限制指令重排序,结合happens-before 规则和强制主内存访问,确保多线程环境下对变量的操作按预期顺序执行,从而保证有序性。
3.3 不保证原子性奥
volatile 变量的复合操作(如 i++) 不具有原子性。
在 JVM 字节码, i++ 操作分为三步(数据加载 数据计算 数据赋值),间隙期不同步非原子操作。
4. 如何正确使用 volatile
单一赋值可以, 复合赋值不可以
状态标志,判断业务是否结束
开销较低的读 写策略
双端锁
5. 总结
5.1 内存屏障是什么
是 一 种 屏 障 指 令,它使得 CPU 或 编译器 对屏障指令的前和后 所发出的内存操作 执行一个排序的约束。
5.2 内存屏障能干嘛
5.3 内存屏障四大指令
在每一个 volatile 写操作前面插入一个 StoreStore 内存屏障
在每一个 volatile 写操作后面插入一个 StoreLoad 内存屏障
在每一个 volatile 读操作前面插入一个 LoadLoad 内存屏障
在每一个 volatile 读操作后面插入一个 LoadStore 内存屏障