看了这篇文章,妈妈再也不会担心面试官问我volatile关键字底层原理了

创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家!


volatile关键字的作用

volatile 关键字是 JVM 提供的最轻量级的同步机制,用来修饰变量,用来保证变量对所有线程可见性。

volatile怎么保证的可见性
无volatile时,内存变化

首先是当 sign 没有 volatitle 修饰时 public boolean sign = false;,线程01对变量进行操作,线程02并不会拿到变化的值。所以程序也就不会输出结果“你坏”。

有volatile时,内存变化

当我们把变量使用volatile修饰时 public volatile boolean sign = false;,线程01对变量进行操作时,会把变量变化的值强制刷新到主内存。当线程02获取值时,会把自己的内存里的sign值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。

反编译解读可见性

类似这样有深度的技术知识,最佳的方式就是深入理解原理,看看它到底做了什么才保证的内存可见性操作。

查看JVM指令

指令:javap -v -p VT

public volatile boolean sign;
  descriptor: Z
  flags: ACC_PUBLIC, ACC_VOLATILE
org.itstack.interview.test.VT();
  descriptor: ()V
  flags:
  Code:
    stack=2, locals=1, args_size=1
      0: aload_0
      1: invokespecial #1 // Method java/lang/Object."<init>":()V
      4: aload_0
      5: iconst_0
      6: putfield #2 // Field sign:Z
      9: return
    LineNumberTable:
      line 35: 0
      line 37: 4
    LocalVariableTable:
      Start Length Slot Name Signature
          0     10 0    this Lorg/itstack/interview/test/VT;
public void run();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=1, args_size=1
      0: aload_0
      1: getfield #2 // Field sign:Z
      4: ifne 10
      7: goto 0
      10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
      13: ldc #4 // String 你坏
      15: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      18: return
    LineNumberTable:
      line 40: 0
      line 42: 10
      line 43: 18
    LocalVariableTable:
      Start Length Slot Name Signature
          0     19 0    this Lorg/itstack/interview/test/VT;
    StackMapTable: number_of_entries = 2
      frame_type = 0 /* same */
      frame_type = 9 /* same */
}

从 JVM 指令码中只会发现多了,ACC_VOLATILE,并没有什么其他的点。所以,也不能看出是怎么实现的可见性。

查看汇编指令

通过 Class 文件查看汇编,需要下载 hsdis-amd64.dll 文件,复制到JAVA_HOME\jre\bin\server 目录下。下载资源如下:

另外是执行命令,包括:

  1. 基础指令:java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

  2. 指定打印:-XX:CompileCommand=dontinline,类名.方法名

  3. 指定打印:-XX:CompileCommand=compileonly,类名.方法名

  4. 输出位置:> xxx

最终使用:java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=dontinline,ApiTest.main -XX:CompileCommand=compileonly,ApiTest.mian

指令可以在 IDEA 中的 Terminal 里使用,也可以到 DOS 黑窗口中使用

另外,为了更简单的使用,我们把指令可以配置到 idea 的 VM options 里,如下图:

配置完成后,不出意外的运行结果如下:

Loaded disassembler from C:\Program Files\Java\jdk1.8.0_161\jre\bin\server\hsdis-amd64.dll
Decoding compiled method 0x0000000003744990:
Code:
Argument 0 is unknown.RIP: 0x3744ae0 Code size: 0x00000110
[Disassembling for mach='amd64']
[Entry Point]
[Constants]
# {method} {0x000000001c853d18} 'getSnapshotTransformerList' '()[Lsun/instrument/TransformerManager$TransformerInfo;' in 'sun/instrument/TransformerManager'
# [sp+0x40] (sp of caller)
0x0000000003744ae0: mov r10d,dword ptr [rdx+8h]
0x0000000003744ae4: shl r10,3h
0x0000000003744ae8: cmp r10,rax
0x0000000003744aeb: jne 3685f60h ; {runtime_call}
0x0000000003744af1: nop word ptr [rax+rax+0h]
0x0000000003744afc: nop
[Verified Entry Point]
0x0000000003744b00: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x0000000003744b07: push rbp
0x0000000003744b08: sub rsp,30h ;*aload_0
; - sun.instrument.TransformerManag
er::getSnapshotTransformerList@0 (line 166)
0x0000000003744b0c: mov eax,dword ptr [rdx+10h]
0x0000000003744b0f: shl rax,3h ;*getfield mTransformerList
; - sun.instrument.TransformerManag
er::getSnapshotTransformerList@1 (line 166)
0x0000000003744b13: add rsp,30h
...

运行结果就是汇编指令,比较多这里就不都放了。我们只观察重点部分:

0x0000000003324cda: mov 0x74(%r8),%edx ;*getstatic state; - VT::run@28 (line 27)
0x0000000003324cde: inc %edx
0x0000000003324ce0: mov %edx,0x74(%r8)
0x0000000003324ce4: lock addl $0x0,(%rsp) ;*putstatic state; - VT::run@33 (line 27)

编译后的汇编指令中,有 volatile 关键字和没有 volatile 关键字,主要差别在于多了一个 lock addl $0x0,(%rsp),也就是 lock 的前缀指令。

lock 指令相当于一个内存屏障,它保证如下三点:

1. 将本处理器的缓存写入内存。

     2. 重排序时不能把后面的指令重排序到内存屏障之前的位置。

3. 如果是写入动作会导致其他处理器中对应的内存无效。

那么,这里的 1、3 就是用来保证被修饰的变量,保证内存可见性。

不加volatile也可见吗

有质疑就要有验证

我们现在再把例子修改下,在 while (!sign) 循环体中添加一段执行代码,如下:

class VT implements Runnable {
  public boolean sign = false;
    public void run() {
      while (!sign) {
        System.out.println("你好");
      }
      System.out.println("你坏");
    }
}

修改后去掉了 volatile 关键字,并在 while 循环中添加一段代码。现在的运行结果是:

你好
你好
你好
vt.sign = true 通知 while (!sign) 结束!
你坏
Process finished with exit code 0

咋样,又可见了吧!

这是因为在没 volatile 修饰时,jvm 也会尽量保证可见性。有 volatile 修饰的时候,一定保证可见性。但可能并非如此,下章节继续深挖,希望可以继续关注我!

总结
  • 最后我们再总结下 volatile,它呢,会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的 CPU 内存设置过期,从主内存中读取最新值。

  • 那么,volatile 防止指令重排的方式是内存屏障,volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore 屏障,StoreStore 屏障是禁止指令重排序的内存屏障,其中Store屏障是每次执行修改数据的时候,强制刷新回主内存。

  • 另外 volatile 并不能解决原子性,如果需要解决原子性问题,需要使用synchronzied 或者 lock,这部分内容在我们后续章节中介绍。

  • 18
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值