乐趣区

关于volatile:面经手册-第14篇volatile-怎么实现的内存可见没有-volatile-一定不可见吗


作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!????

一、码场心得

你是个能享乐的人吗?

从前的能享乐大多指的体力劳动的苦,但当初的能享乐曾经包含太多维度,包含:读书学习 & 寂寞的苦 深度思考 & 脑力的苦 自律习惯 & 修行的苦 自控能力 & 放弃的苦 抬头做人 & 尊严的苦

尽管这些苦摆在眼前,但大多数人还是喜爱吃简略的苦。熬夜加班、日复一日、反复昨天、CRUD,最初身材发胖、体质降落、能力有余、自抱自泣!所以有些苦能不吃就不吃,要吃就吃那些有成长价值的苦。

明天你写博客了吗?

如果一件小事能保持 5 年以上,那你肯定是很了不起的人。是的,很了不起。人最难的就是想分明了但做不到,或者偶然做到长期做不到。

其实大多数走在研发路上的搭档们,都晓得本人该致力,但明明下好了的信心就是保持不了多久。就像你是否也想过要写技术博客,做技术积攒。直到有一天被瓶颈限度在困局中才会焦急,但这时候在想破局就真的很难了!

二、面试题

谢飞机,小记,飞机趁着周末,吃完火锅。又去约面试官喝茶了!

谢飞机:嗨,我在这,这边,这边。

面试官:你怎么又来了,最近学的不错了?

谢飞机:还是想来大厂,别害羞,面我吧!

面试官:我如同是你补课老师 … 既然来了,就问问你吧!volatile 是干啥的?

谢飞机:啊,volatile 是保障变量对所有线程的可见性的。

面试官:那 volatile 能够解决原子性问题吗?

谢飞机:不能够!

面试官:那 volatile 的底层原理是如何实现的呢?

谢飞机 :…,这! 面试官,刚问两个题就甩雷,你是不家里有事要忙?

面试官:你管我!

三、volatile 解说

1. 可见性案例

public class ApiTest {public static void main(String[] args) {final VT vt = new VT();

        Thread Thread01 = new Thread(vt);
        Thread Thread02 = new Thread(new Runnable() {public void run() {
                try {Thread.sleep(3000);
                } catch (InterruptedException ignore) { }
                vt.sign = true;
                System.out.println("vt.sign = true 告诉 while (!sign) 完结!");
            }
        });

        Thread01.start();
        Thread02.start();}

}

class VT implements Runnable {

    public boolean sign = false;

    public void run() {while (!sign) { }
        System.out.println("你坏");
    }
}

这段代码 ,是两个线程操作一个变量,程序冀望当 sign 在线程 Thread01 被操作 vt.sign = true 时,Thread02 输入 你坏

但实际上这段代码永远不会输入 你坏,而是始终处于死循环。这是为什么呢?接下来咱们就一步步解说和验证。

2. 加上 volatile 关键字

咱们把 sign 关键字加上 volatitle 形容,如下:

class VT implements Runnable {

    public volatile boolean sign = false;

    public void run() {while (!sign) { }
        System.out.println("你坏");
    }
}

测试后果

vt.sign = true 告诉 while (!sign) 完结!你坏

Process finished with exit code 0

volatile 关键字是 Java 虚拟机提供的的最轻量级的同步机制,它作为一个修饰符呈现,用来润饰变量,然而这里不包含局部变量哦

在增加 volatile 关键字后,程序就合乎预期的输入了 你坏。从咱们对 volatile 的学习认知能够晓得。volatile 关键字是 JVM 提供的最轻量级的同步机制,用来润饰变量,用来保障变量对所有线程可见性。

正在润饰后能够让字段在线程见可见,那么这个属性被批改值后,能够及时的在另外的线程中做出相应的反馈。

3. volatile 怎么保障的可见性

3.1 无 volatile 时,内存变动

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

3.2 有 volatile 时,内存变动

当咱们把变量应用 volatile 润饰时 public volatile boolean sign = false;,线程 01 对变量进行操作时,会把变量变动的值强制刷新的到主内存。当线程 02 获取值时,会把本人的内存里的 sign 值过期掉,之后从主内存中读取。所以增加关键字后程序如预期输入后果。

4. 反编译解毒可见性

相似这样有深度的技术常识,最佳的形式就是深刻了解原理,看看它到底做了什么才保障的内存可见性操作。

4.1 查看 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,并没有什么其余的点。所以,也不能看出是怎么实现的可见性。

4.2 查看汇编指令

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

  • http://vorboss.dl.sourceforge.net/project/fcml/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip
  • http://vorboss.dl.sourceforge.net/project/fcml/fcml-1.1.1/hsdis-1.1.1-win32-i386.zip

另外是执行命令,包含:

  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.TransformerManager::getSnapshotTransformerList@0 (line 166)

  0x0000000003744b0c: mov     eax,dword ptr [rdx+10h]
  0x0000000003744b0f: shl     rax,3h            ;*getfield mTransformerList
                                                ; - sun.instrument.TransformerManager::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 就是用来保障被润饰的变量,保障内存可见性。

5. 不加 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、写操作后增加 StoreLoad、读操作前增加 LoadLoad、读操作后增加 LoadStore),也就是四个地位,来保障重排序时不能把内存屏障前面的指令重排序到内存屏障之前的地位。
  • 另外 volatile 并不能解决原子性,如果须要解决原子性问题,须要应用 synchronzied 或者 lock,这部分内容在咱们后续章节中介绍。

五、系列举荐

  • 握草,你居然在代码里下毒!
  • HashMap 外围常识,扰动函数、负载因子、扩容链表拆分
  • ThreadLocal 技术栈深度学习
  • 一次代码评审,差点过不了试用期!
  • Netty 仿桌面版微信实战
退出移动版