【Java】volatile关键字

InterviewCoder

# 【Java】volatile 关键字

# volatile

# 1. 可见性案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 描述,如下:

1
2
3
4
5
6
7
8
9
10
class VT implements Runnable {

public volatile boolean sign = false;

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

测试结果

1
2
3
4
vt.sign = true 通知 while (!sign) 结束!
你坏

Process finished with exit code 0

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

在添加 volatile 关键字后,程序就符合预期的输出了 你坏。从我们对 volatile 的学习认知可以知道。volatile 关键字是 JVM 提供的最轻量级的同步机制,用来修饰变量,用来保证变量对所有线程可见性。

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

# 3. volatile 怎么保证的可见性

# 3.1 无 volatile 时,内存变化

无volatile时,内存变化

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

# 3.2 有 volatile 时,内存变化

有volatile时,内存变化

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

# 4. 反编译解毒可见性

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

# 4.1 查看 JVM 指令

指令javap -v -p VT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 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目录下 。下载资源如下:

另外是执行命令,包括:

  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 里,如下图:

Idea VM options 配置编译指令

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
...

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

1
2
3
4
5
6
7
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 也可见吗

1
有质疑就要有验证

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

1
2
3
4
5
6
7
8
9
10
11
12
class VT implements Runnable {

public boolean sign = false;

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

}

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

1
2
3
4
5
6
7
8
...
你好
你好
你好
vt.sign = true 通知 while (!sign) 结束!
你坏

Process finished with exit code 0

咋样,又可见了吧!

这是因为在没 volatile 修饰时,jvm 也会尽量保证可见性。有 volatile 修饰的时候,一定保证可见性。

# 总结

  • volatile 会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的 CPU 内存设置过期,从主内存中读取最新值。
  • 那么,volatile 如何防止指令重排也是内存屏障,volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore 屏障,也就是四个位置,来保证重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置。
  • 另外 volatile 并不能解决原子性问题,即对于复合操作(比如 i++ 等)无法保证线程安全,如果需要解决原子性问题,需要使用 synchronzied 或者 lock。
  • 使用 volatile 虽然可以提供比较高的可见性和顺序性的保障,但并不是绝对可靠的,还需要根据具体的业务场景和代码需求来进行综合考虑和评估。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

评论