特点:
- 可见性
- 有序性(有时需要禁用重排)
内存语义:
- 当写一个 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
}
}
屏障类型 | 指令实示例 | 说明 |
LoadLoad | Load1; LoadLoad; Load2 | 保证 load1 的读取操作在 load2及后续读取操作之前执行 |
StoreStore | Store1; StoreStore; Store2 | 在 store2 及其后的系操作执行前,保证 store1 的写操作已刷新到主内存 |
LoadStore | Load1; LoadStore; Store2 | 在 store2 及其后的写操作执行前,保证 load1 的读操作已经读取结束 |
StoreLoad | Store1;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;
}