Volatile从浅到深

目录

一、Volatile可见性问题

二、Idea查看字节码

三、查看汇编代码

四、Volatile 与内存屏障 Memory Barrier

大神 Doug Lea 写的一篇Java内存屏障说明:


一、Volatile可见性问题

public class TestVolatileVisibility {
    public static /* volatile */int found = 0;
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("A waiting money,Begin...");
            // 加上 volatile 能让每次都去内存读取数据到CPU cache
            while (0 == found){
            }
            System.out.println("A get the money, End ...");
        },"myThread-A").start();

        new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (Exception e){
                e.printStackTrace();
            }
            System.out.println("B send money");
            change();

        },"myThread-B").start();
    }
    public static void change(){
        found = 1;
    }
}

输出:

A waiting money,Begin...
B send money
A get the money, End ...

如果不加关键字 volatile,那么A线程将一直等待,因为A线程的found一直是去cpu缓存中读取,不会去内存中重新刷出来。从而导致一直 while循环。

volatile保证了可见性,能保证每次都能去内存读,而不单单取CPU缓存中的数据。

二、Idea查看字节码

下载插件: jclasslib Bytecode viewer

或者直接 javap -v -p class文件名(不要.class 后缀)运行

IDEA -- Reference -- Plugins

先Build一下,生成Class文件

View -- Show Bytecode With Jclasslib

可以发现,加了 Volatile的时候,从字节码中,可以看到 Access flags中是有 volatile,

自己去比对就知道,java代码中,其他地方都没区别,加不加 volatile,在字节码层面只在 Access flags这个地方有区别。

三、查看汇编代码

Mac下查看已安装的jdk版本及其安装目录

localhost:bin jeblin$ /usr/libexec/java_home -V
Matching Java Virtual Machines (1):
    1.8.0_231, x86_64:	"Java SE 8"	/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home

工具 : hsdis-amd64.dylib

    将上述两个文件放在你的 jre/bin 路径下的路径里。

 /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/hsdis-amd64.dylib

idea配置 VM options:

-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*TestVolatileVisibility.change

这块(TestVolatileVisibility.change)换成你的类名+方法名字

 然后运行,或者直接命令行运行Java

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*VolatileBarrierExample.readAndWrite 包名.类名

 

0x00000001128318ad: lock addl $0x0,(%rsp)     ;*putstatic found
                                                ; - indi.sword.util.basic.Thread.Volatile.TestVolatileVisibility::change@1 (line 31)

如果没有Volatile关键字,那么就没有 lock

  0x0000000109ab3565: movl   $0x1,0x68(%rsi)    ;*putstatic found
                                                ; - indi.sword.util.basic.Thread.Volatile.TestVolatileVisibility::change@1 (line 31)

可见其本质是通过一个lock指令来实现的。那么lock是什么意思呢?

lock是一种控制指令,在多处理器环境下,lock 汇编指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果。

查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。(我的理解是通知shared状态的cpu强制去消费invalid queue队列,把位于cpu Cache中的共享的变量 CacheLine置为Invalid,然后重新去内存中读取最新的值)所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。

也就是Volatile解决了可见性的问题

所以,它的作用是

  1. 锁住主存
  2. 任何读必须在写完成之后再执行
  3. 使其它线程这个值的栈缓存失效

类似于前面是storestore,后面是storeload

四、Volatile 与内存屏障 Memory Barrier

Volatile使用内存屏障,内存屏障的作用,是为了阻止各阶段的乱序优化

Java内存模型关于重排序的规定,总结后如下表所示:

表格中的“是否允许重排序”,意思是指,第一项操作和第二项操作会不会被重排序,也就是这俩操作会不会被乱序优化

大神 Doug Lea 写的一篇Java内存屏障说明:

The JSR-133 Cookbook

竖的是第一个操作,横的是第二个操作。

LoadLoad是啥含义? 左边的Load表示第一个操作是读,右边的Load表示第二个操作是读。其他以此类推。

Required barriers2nd operation
1st operationNormal LoadNormal StoreVolatile Load
MonitorEnter
Volatile Store
MonitorExit
Normal LoadLoadStore
Normal StoreStoreStore
Volatile Load
MonitorEnter
LoadLoadLoadStoreLoadLoadLoadStore
Volatile Store
MonitorExit
StoreLoadStoreStore

Here's an example showing placements.

JavaInstructions
class X {
  int a, b;
  volatile int v, u;
  void f() {
    int i, j;
   
    i = a;
    j = b;
    i = v;
   
    j = u;
   
    a = i;
    b = j;
   
    v = i;
   
    u = j;
   
    i = u;
   
   
    j = b;
    a = i;
  }
}
     






load a
load b
load v
   LoadLoad
load u
   LoadStore
store a
store b
   StoreStore
store v
   StoreStore
store u
   StoreLoad
load u
   LoadLoad
   LoadStore
load b
store a

从上表我们可以看出:

  1. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。(也就是Volatile 读完之后,才去干后面的事情)
  2. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。(也就是Volatile写之前,前面事情已经干完了)
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。(写的第2条已经说了,不能)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值