【Java 多线程】Volatile 详解

Volatile

为了平衡 CPU、内存和 IO 设备之间的读写速度,充分利用 CPU 的高性能,我们的计算机体系结构、操作系统以及编译程序都做了很多的优化:

  • CPU 增加了高速缓存来平衡 CPU 和内存之间的速度差异,这也就导致可见性问题
  • 操作系统增加了进程、线程来分时复用 CPU,进而平衡 CPU 和 IO 设备之间的速度,这就带来了原子性问题
  • 编译程序优化指令执行顺序,使得缓存能够更加充分的利用。这就带来了有序性问题。

Volatile 是 Java 的一个关键字,他可以防止重排序,解决有序性问题,还可以通过缓存一致性协议实现可见性

Volatile 可见性实现

可见性的问题主要指的就是,一个线程修改了共享变量的值,另一个内存却看不到。

对一个 volatile 变量的赋值程序通过反编译可以看到,在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令

public class Test {
    private volatile int a;
    public void update() {
        a = 1;
    }
    public static void main(String[] args) {
        Test test = new Test();
        test.update();
    }
}
  0x0000000002951563: and    $0xffffffffffffff87,%rdi
  0x0000000002951567: je     0x00000000029515f8
  0x000000000295156d: test   $0x7,%rdi
  0x0000000002951574: jne    0x00000000029515bd
  0x0000000002951576: test   $0x300,%rdi
  0x000000000295157d: jne    0x000000000295159c
  0x000000000295157f: and    $0x37f,%rax
  0x0000000002951586: mov    %rax,%rdi
  0x0000000002951589: or     %r15,%rdi
  0x000000000295158c: lock cmpxchg %rdi,(%rdx)  //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
  0x0000000002951591: jne    0x0000000002951a15
  0x0000000002951597: jmpq   0x00000000029515f8
  0x000000000295159c: mov    0x8(%rdx),%edi
  0x000000000295159f: shl    $0x3,%rdi
  0x00000000029515a3: mov    0xa8(%rdi),%rdi
  0x00000000029515aa: or     %r15,%rdi

lock 前缀的指令在多核处理器下会触发两件事情:

  • 将当前处理器缓存行数据写入内存
  • 写回内存的操作会让其他处理器的缓存中缓存了该地址的数据的缓存失效

对于普通的写操作,处理器不直接和内存进行通信,而是直接将数据写进缓存中,但并不知道什么时候写进内存中。

对于一个 volatile 变量的写操作,JVM 会向处理器发送一条 lock 指令,将这个变量所在的缓存行的数据写回内存中。

为了让各个处理器的缓存是一致的,实现了缓存一致性协议(EMSI),每个处理器会在总线上嗅探自己的缓存是不是过期了,档处理器发现自己的缓存有过期的,则将自己对应的缓存行状态设置为无效,当处理器重新读取这个数据的时候,会从内存中重新读取。

Volatile 有序性实现

happen-before 规则中有一条是 volatile 变量规则:对于一个 volatile 域的写,总是 happen-before 于任意后续对这个 volatile 的读

volatile 可以禁止指令的重排序,JMM提供了内存屏障来阻止重排序

JMM 提供了四种内存屏障:

  • StoreStore 屏障:禁止上面的普通写和下面的 volatile 写重排序;
  • StoreLoad 屏障:禁止上面的 Volatile 写和下面可能存在的 Volatile 读/写重排序;
  • LoadLoad 屏障:禁止下面所有的普通读操作和上面的 Volatile 读重排序
  • LoadStore 屏障:禁止下面所有的普通写操作和上面的 Volatile 读重排序

对于编译器来说,发现一个最优布置来最小化插入屏障的总数是不可能的,为此 JMM 采取了保守策略:

  • 对于每个 Volatile 写操作,在前面插入一个 StoreStore 屏障
  • 对于每个 Volatile 写操作,在后面插入一个 StoreLoad 屏障
  • 对于每个 Volatile 读操作,在后面插入一个 LoadLoad 屏障
  • 对于每个 Volatile 读操作,在后面插入一个 LoadStore 屏障

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值