本文章已授权微信公众号郭霖(guolin_blog)转载。
本文章讲解的内容是深入了解volatile关键字,建议对着示例项目阅读文章,示例项目链接如下:
查看汇编代码的hsdis-amd64.dylib文件链接如下:
汇编代码链接如下:
-
双重检查锁定(DCL)单例模式使用关键字volatile修饰变量的汇编代码:SingletonAssemblyCodeWithVolatile.log
-
双重检查锁定(DCL)单例模式不使用关键字volatile修饰变量的汇编代码:SingletonAssemblyCodeWithNoVolatile.log
关键字volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被关键字volatile修饰之后,它有如下两个特性:
- 保证了这个变量对所有线程的可见性
- 禁止指令重排序优化
保证变量对所有线程的可见性
关键字volatile可以保证变量对所有线程的可见性,也就是当一个线程修改了这个变量的值,其他线程能够立即得到修改的值。普通变量是做不到这样,普通变量的值需要通过主内存在线程之间传递,举个例子:线程A修改一个普通变量的值,然后传送给主内存,另外一个线程B需要等到传送完主内存后才能够从主内存进行读取操作,这样变量最新的值才会对线程B可见。
先看下如下例子,代码如下所示:
/**
* Created by TanJiaJun on 2020-08-16.
*/
class VolatileDemo {
private static final int THREADS_COUNT = 10;
private static volatile int value = 0;
private static void increase() {
// 对value变量进行自增操作
value++;
}
public static void main(String[] args) {
// 创建10个线程
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++)
// 每个线程对value变量进行1000次自增操作
increase();
});
threads[i].start();
}
// 主线程等待子线程运行结束
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("value的值:" + value);
}
}
这段代码的意思是发起10个线程,然后每个线程对value变量进行1000次自增操作,如果这段代码正确地并发操作,最后的结果value的值应该是10000,但是实际上多次运行后,value的值都是小于等于10000的值。
这段代码中increase方法调用i++,也就是i = i + 1,它不是原子性操作,Java内存模型直接保证的原子性变量操作包括read、load、assign、use、store和write,我们可以认为基本数据类型的读写都具备原子性,有个例外就是long和double的非原子性协定,不过我们无须太过在意,虽然Java内存模型允许虚拟机不把long和double的变量的读写实现为原子性操作,但是现在的商用虚拟机都几乎把这些操作实现为原子性操作,原子性操作是指执行一系列操作,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况,举个例子:i = 1就是个原子性操作,但是i = i + 1就不是原子性操作,因为这个操作是由多条字节码指令构成的,我用Javap反编译上面的示例代码,先找到生成的Class文件,路径是/Users/tanjiajun/IdeaProjects/VolatileDemo/out/production/VolatileDemo/VolatileDemo.class,就是在VolatileDemo目录下的out文件夹中,然后执行javap -p -v VolatileDemo命令,生成如下字节码:
Classfile /Users/tanjiajun/IdeaProjects/VolatileDemo/out/production/VolatileDemo/VolatileDemo.class
Last modified 2020年8月19日; size 2160 bytes
SHA-256 checksum 41886220d7539d7ff90dfcd4a870539affb765699bcef3cb07119658a4a5d1a3
Compiled from "VolatileDemo.java"
class VolatileDemo
minor version: 0
major version: 57
flags: (0x0020) ACC_SUPER
this_class: #8 // VolatileDemo
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 5, attributes: 3
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // VolatileDemo.value:I
#8 = Class #10 // VolatileDemo
#9 = NameAndType #11:#12 // value:I
#10 = Utf8 VolatileDemo
#11 = Utf8 value
#12 = Utf8 I
#13 = Class #14 // java/lang/Thread
#14 = Utf8 java/lang/Thread
#15 = InvokeDynamic #0:#16 // #0:run:()Ljava/lang/Runnable;
#16 = NameAndType #17:#18 // run:()Ljava/lang/Runnable;
#17 = Utf8 run
#18 = Utf8 ()Ljava/lang/Runnable;
#19 = Methodref #13.#20 // java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
#20 = NameAndType #5:#21 // "<init>":(Ljava/lang/Runnable;)V
#21 = Utf8 (Ljava/lang/Runnable;)V
#22 = Methodref #13.#23 // java/lang/Thread.start:()V
#23 = NameAndType #24:#6 // start:()V
#24 = Utf8 start
#25 = Methodref #13.#26 // java/lang/Thread.join:()V
#26 = NameAndType #27:#6 // join:()V
#27 = Utf8 join
#28 = Class #29 // java/lang/InterruptedException
#29 = Utf8 java/lang/InterruptedException
#30 = Methodref #28.#31 // java/lang/InterruptedException.printStackTrace:()V
#31 = NameAndType #32:#6 // printStackTrace:()V
#32 = Utf8 printStackTrace
#33 = Fieldref #34.#35 // java/lang/System.out:Ljava/io/PrintStream;
#34 = Class #36 // java/lang/System
#35 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = InvokeDynamic #1:#40 // #1:makeConcatWithConstants:(I)Ljava/lang/String;
#40 = NameAndType #41:#42 // makeConcatWithConstants:(I)Ljava/lang/String;
#41 = Utf8 makeConcatWithConstants
#42 = Utf8 (I)Ljava/lang/String;
#43 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#44 = Class #46 // java/io/PrintStream
#45 = NameAndType #47:#48 // println:(Ljava/lang/String;)V
#46 = Utf8 java/io/PrintStream
#47 = Utf8 println
#48 = Utf8 (Ljava/lang/String;)V
#49 = Methodref #8.#50 // VolatileDemo.increase:()V
#50 = NameAndType #51:#6 // increase:()V
#51 = Utf8 increase
#52 = Utf8 THREADS_COUNT
#53 = Utf8 ConstantValue
#54 = Integer 10
#55 = Utf8 Code
#56 = Utf8 LineNumberTable
#57 = Utf8 LocalVariableTable
#58 = Utf8 this
#59 = Utf8 LVolatileDemo;
#60 = Utf8 main
#61 = Utf8 ([Ljava/lang/String;)V
#62 = Utf8 i
#63 = Utf8 e
#64 = Utf8 Ljava/lang/InterruptedException;
#65 = Utf8 thread
#66 = Utf8 Ljava/lang/Thread;
#67 = Utf8 args
#68 = Utf8 [Ljava/lang/String;
#69 = Utf8 threads
#70 = Utf8 [Ljava/lang/Thread;
#71 = Utf8 StackMapTable
#72 = Class #70 // "[Ljava/lang/Thread;"
#73 = Class #68 // "[Ljava/lang/String;"
#74 = Utf8 lambda$main$0
#75 = Utf8 j
#76 = Utf8 <clinit>
#77 = Utf8 SourceFile
#78 = Utf8 VolatileDemo.java
#79 = Utf8 BootstrapMethods
#80 = MethodHandle 6:#81 // REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#81 = Methodref #82.#83 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#82 = Class #84 // java/lang/invoke/LambdaMetafactory
#83 = NameAndType #85:#86 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#84 = Utf8 java/lang/invoke/LambdaMetafactory
#85 = Utf8 metafactory
#86 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#87 = MethodType #6 // ()V
#88 = MethodHandle 6:#89 // REF_invokeStatic VolatileDemo.lambda$main$0:()V
#89 = Methodref #8.#90 // VolatileDemo.lambda$main$0:()V
#90 = NameAndType #74:#6 // lambda$main$0:()V
#91 = MethodHandle 6:#92 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#92 = Methodref #93.#94 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#93 = Class #95 // java/lang/invoke/StringConcatFactory
#94 = NameAndType #41:#96 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#95 = Utf8 java/lang/invoke/StringConcatFactory
#96 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#97 = String #98 // value的值:\u0001
#98 = Utf8 value的值:\u0001
#99 = Utf8 InnerClasses
#100 = Class #101 // java/lang/invoke/MethodHandles$Lookup
#101 = Utf8 java/lang/invoke/MethodHandles$Lookup
#102 = Class #103 // java/lang/invoke/MethodHandles
#103 = Utf8 java/lang/invoke/MethodHandles
#104 = Utf8 Lookup
{
private static final int THREADS_COUNT;
descriptor: I
flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
private static volatile int value;
descriptor: I
flags: (0x004a) ACC_PRIVATE, ACC_STATIC, ACC_VOLATILE
VolatileDemo();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LVolatileDemo;
private static void increase();
descriptor: ()V
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #7 // Field value:I
3: iconst_1
4: iadd
5: putstatic #7 // Field value:I
8: return
LineNumberTable:
line 12: 0
line 13: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=7, args_size=1
0: bipush 10
2: anewarray #13 // class java/lang/Thread
5: astore_1
6: iconst_0
7: istore_2
8: iload_2
9: bipush 10
11: if_icmpge 41
14: aload_1
15: iload_2
16: new #13 // class java/lang/Thread
19: dup
20: invokedynamic #15, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
25: invokespecial #19 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
28: aastore
29: aload_1
30: iload_2
31: aaload
32: invokevirtual #22 // Method java/lang/Thread.start:()V
35: iinc 2, 1
38: goto 8
41: aload_1
42: astore_2
43: aload_2
44: arraylength
45: istore_3
46: iconst_0
47: istore 4
49: iload 4
51: iload_3
52: if_icmpge 82
55: aload_2
56: iload 4
58: aaload
59: astore 5
61: aload 5
63: invokevirtual #25 // Method java/lang/Thread.join:()V
66: goto 76
69: astore 6
71: aload 6
73: invokevirtual #30 // Method java/lang/InterruptedException.printStackTrace:()V
76: iinc 4, 1
79: goto 49
82: getstatic #33 // Field java/lang/System.out:Ljava/io/PrintStream;
85: getstatic #7 // Field value:I
88: invokedynamic #39, 0 // InvokeDynamic #1:makeConcatWithConstants:(I)Ljava/lang/String;
93: invokevirtual #43 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
96: return
Exception table:
from to target type
61 66 69 Class java/lang/InterruptedException
LineNumberTable:
line 17: 0
line 18: 6
line 19: 14
line 24: 29
line 18: 35
line 27: 41
line 29: 61
line 32: 66
line 30: 69
line 31: 71
line 27: 76
line 34: 82
line 35: 96
LocalVariableTable:
Start Length Slot Name Signature
8 33 2 i I
71 5 6 e Ljava/lang/InterruptedException;
61 15 5 thread Ljava/lang/Thread;
0 97 0 args [Ljava/lang/String;
6 91 1 threads [Ljava/lang/Thread;
StackMapTable: number_of_entries = 6
frame_type = 253 /* append */
offset_delta = 8
locals = [ class "[Ljava/lang/Thread;", int ]
frame_type = 250 /* chop */
offset_delta = 32
frame_type = 254 /* append */
offset_delta = 7
locals = [ class "[Ljava/lang/Thread;", int, int ]
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class "[Ljava/lang/Thread;", class "[Ljava/lang/Thread;", int, int, class java/lang/Thread ]
stack = [ class java/lang/InterruptedException ]
frame_type = 250 /* chop */
offset_delta = 6
frame_type = 248 /* chop */
offset_delta = 5
private static void lambda$main$0();
descriptor: ()V
flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: sipush 1000
6: if_icmpge 18
9: invokestatic #49 // Method increase:()V
12: iinc 0, 1
15: goto 2
18: return
LineNumberTable:
line 20: 0
line 22: 9
line 20: 12
line 23: 18
LocalVariableTable:
Start Length Slot Name Signature
2 16 0 j I
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 250 /* chop */
offset_delta = 15
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #7 // Field value:I
4: return
LineNumberTable:
line 8: 0
}
SourceFile: "VolatileDemo.java"
BootstrapMethods:
0: #80 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#87 ()V
#88 REF_invokeStatic VolatileDemo.lambda$main$0:()V
#87 ()V
1: #91 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#97 value的值:\u0001
InnerClasses:
public static final #104= #100 of #102; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
然后找到对应的increase方法的字节码,字节码如下所示:
private static void increase();
descriptor: ()V
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #7 // Field value:I
3: iconst_1
4: iadd
5: putstatic #7 // Field value:I
8: return
LineNumberTable:
line 12: 0
line 13: 8
可以看到value++是由四条指令构成的,分别是getstatic、iconst_1、iadd和putstatic,getstatic指令是获取静态字段value的值并且放入操作栈顶,iconst_1指令是把常量1放入操作栈顶,iadd指令是把当前操作栈顶中两个值相加并且把结果放入操作栈顶,putstatic指令是把操作栈顶的结果赋值给静态变量value,关键字volatile可以保证执行getstatic指令后的值是正确的,如果在并发环境下,可能有其他线程在执行iconst_1指令或者iadd指令时,增加了value的值,导致操作栈顶的值就变成了过期的数据,在执行putstatic指令后可能把较小的value的值同步回主内存中,导致不能得到正确的结果。
从上面的例子可以得知,volatile变量只保证可见性,以下两条规则的运算环境可以保证这些操作的原子性:
- 只有单条线程修改变量的值,运算结果不依赖变量当前的值,也就是说不依赖产生的中间结果。
- 变量不需要与其他的状态变量共同参与不变约束。
如果不符合以上两条规则的话,就需要通过加锁来保证这些操作的原子性,可以使用关键字synchronized或者java.util.concurrent中的原子类。
禁止指令重排序优化
Java内存模型中的一个语义是线程内表现为串行的语义(Within-Thread As-If-Serial Semantics),它是指普通变量只能保证在该方法在执行过程中所有依赖赋值结果的地方都能得到正确的结果,但是不保证变量的赋值操作的顺序和程序代码中的执行顺序是一致的。举个例子,代码如下所示:
int i = 1;
int j = 2;
int k = i + j;
上面这段代码大概执行了以下步骤:
- 将常量1赋值给i
- 将常量2赋值给j
- 取到i的值
- 取到j的值
- 将i的值和j的值相加后赋值给k
在上面这五个步骤中,步骤1可能会和步骤2和步骤4重排序,步骤2可能会和步骤1和步骤3重排序,步骤3可能会和步骤2和步骤4重排序,步骤4可能会和步骤1和步骤3重排序,但是步骤1、步骤3和步骤5之间不能重排序,步骤2、步骤4和步骤5之间不能重排序,因为它们之间存在依赖关系,一旦重排序,线程表现为串行的语义将无法得到保证。
再看个例子,使用双重检查锁定(DCL)实现单例模式,代码如下所示:
/**
* Created by TanJiaJun on 2020/8/23.
*/
class Singleton {
// 用关键字volatile修饰变量sInstance,禁止指令重排序优化
private static volatile Singleton sInstance;
// 私有构造方法
private Singleton() {
// 防止通过反射调用构造方法导致单例失效
if (sInstance != null)
throw new RuntimeException("Cannot construct a singleton more than once.");
}
// 获取单例的方法
public static Singleton getInstance() {
// 第一次判断sInstance是否为空,用于判断是否需要同步,提高性能和效率
if (sInstance == null) {
// 使用synchronized修饰代码块,取Singleton的Class对象作为锁对象
synchronized (Singleton.class) {
// 第二次判断sInstance是否为空,用于判断是否已经创建实例
if (sInstance == null) {
// 创建Singleton对象
sInstance = new Singleton();
}
}
}
// 返回sInstance
return sInstance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
然后使用HSDIS插件反汇编上面的代码,我只截取了对变量sInstance赋值(第25行)的那部分汇编代码,如果想要看全部的汇编代码,可以在查看SingletonAssemblyCodeWithVolatile.log,汇编代码如下所示:
0x000000011b33f4c7: mov 0x38(%rsp),%rax
0x000000011b33f4cc: movabs $0x61ff0ac48,%rdx ; {oop(a 'java/lang/Class'{0x000000061ff0ac48} = 'Singleton')}
0x000000011b33f4d6: movsbl 0x30(%r15),%esi
0x000000011b33f4db: cmp $0x0,%esi
0x000000011b33f4de: jne 0x000000011b33f6e9
0x000000011b33f4e4: mov %rax,%r10
0x000000011b33f4e7: shr $0x3,%r10
0x000000011b33f4eb: mov %r10d,0x70(%rdx)
0x000000011b33f4ef: lock addl $0x0,-0x40(%rsp)
0x000000011b33f4f5: mov %rdx,%rsi
0x000000011b33f4f8: xor %rax,%rsi
0x000000011b33f4fb: shr $0x15,%rsi
0x000000011b33f4ff: cmp $0x0,%rsi
0x000000011b33f503: jne 0x000000011b33f708 ;*putstatic sInstance {reexecute=0 rethrow=0 return_oop=0}
; - Singleton::getInstance@24 (line 25)
然后把代码中的关键字volatile去掉,再生成汇编代码,我只截取了对变量sInstance赋值(第25行)的那部分汇编代码,如果想要看全部的汇编代码,可以在查看SingletonAssemblyCodeWithNoVolatile.log,汇编代码如下所示:
0x0000000116f2a4c7: mov 0x38(%rsp),%rax
0x0000000116f2a4cc: movabs $0x61ff0acb8,%rdx ; {oop(a 'java/lang/Class'{0x000000061ff0acb8} = 'Singleton')}
0x0000000116f2a4d6: movsbl 0x30(%r15),%esi
0x0000000116f2a4db: cmp $0x0,%esi
0x0000000116f2a4de: jne 0x0000000116f2a6e1
0x0000000116f2a4e4: mov %rax,%r10
0x0000000116f2a4e7: shr $0x3,%r10
0x0000000116f2a4eb: mov %r10d,0x70(%rdx)
0x0000000116f2a4ef: mov %rdx,%rsi
0x0000000116f2a4f2: xor %rax,%rsi
0x0000000116f2a4f5: shr $0x15,%rsi
0x0000000116f2a4f9: cmp $0x0,%rsi
0x0000000116f2a4fd: jne 0x0000000116f2a700 ;*putstatic sInstance {reexecute=0 rethrow=0 return_oop=0}
; - Singleton::getInstance@24 (line 25)
通过对比可以发现,如果变量sInstance被关键字volatile修饰,会在赋值(mov %r10d,0x70(%rdx))后多执行一个lock addl 0x0,-0x40(%rsp)(%rsp是堆栈指针寄存器,通常会指向栈顶位置,堆栈的pop操作和push操作是通过改变%rsp的值来移动堆栈指针的位置来实现)是一个空操作,查询IA32手册可得知,使用这个空操作,而不是使用空操作指令nop是因为前缀lock不允许配合nop指令使用,其中前缀lock,查询IA32手册可得知,它的作用是使得本CPU的缓存写入内存,相当于对缓存中的变量执行store操作和write操作,这个写入动作可以让其他CPU或者别的内核无效化(Invalidata)其缓存,可以让前面对被关键字volatile修饰的变量的修改对其他线程立即可见。
内存屏障
内存屏障(Memory Barrier),也称为内存栅栏、内存栅障和屏障指令等,是一类同步屏障指令,它使得CPU或者编译器在对内存进行操作的时候,严格按照一定的顺序执行,大多数现代计算机为了提高性能而采用乱序执行,它就可以使内存屏障前的指令和内存屏障后的指令不会因为系统优化而导致乱序执行。
内存屏障的语义是内存屏障前的所有写操作都要写入内存,内存屏障后的所有读操作都可以获得同步屏障之前的读操作的结果。
内存屏障可以分为以下四种类型:
-
LoadLoad屏障:
序列:①Load1②LoadLoad③Load2
确保Load1要载入的数据能够在被Load2和后面的load指令载入数据前载入。
-
StoreStore屏障:
序列:①Store1②StoreStore③Store2
确保Store1要存储的数据能够在Store2和后面的store指令同步回主内存前对其它处理器可见。
-
LoadStore屏障:
序列:①Load1②LoadStore③Store2
确保Load1要载入的数据能够在Store2和后面的store指令同步回主内存前载入。
-
StoreLoad屏障:
序列:①Store1②StoreLoad③Load2
确保Store1要存储的数据能够在Load2和后面的load指令载入数据前对其它处理器可见。它是这四种内存屏障中开销最大的,它也是一个万能屏障,具有其它三种内存屏障的功能。
下图展示了这些内存屏障如何符合JSR-133排序规则:
MemoryBarrierRule.png举个例子,代码如下所示:
/**
* Created by TanJiaJun on 2020/8/23.
*/
class MemoryBarrierTest {
private int a, b;
private volatile int c, d;
private void test() {
int i, j;
i = a; // load a
j = b; // load b
i = c; // load c
// LoadLoad
j = d; // load d
// LoadStore
a = i; // store a
b = j; // store b
// StoreStore
c = i; // store c
// StoreStore
d = j; // store d
// StoreLoad
i = d; // load d
// LoadLoad
// LoadStore
j = b; // load b
a = i; // store a
}
}
另外,为了保证关键字final的特殊语义,会在下面的序列中加入内存屏障:
①x.finalField = v;②StoreStore③sharedRef = x;
总结
总结下Java内存模型中对被关键字volatile修饰的变量进行read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)和write(写入)操作定义的特殊规则:
-
假设有一个线程A,有一个被关键字volatile修饰的变量i;只有当线程A对变量i执行的前一个操作是load操作的时候,线程A才能对变量i进行use操作;并且,只有线程A对变量i执行的后一个操作是use操作的时候,线程A才能对变量i执行load操作,也就是说,线程A对变量i执行use操作是和对其执行read操作和load操作相关联的,它们都必须要连续一起出现。
这条规则要求在工作内存中,每次使用volatile变量都必须从主内存中刷新最新的值,用于保证能看见其他线程对volatile变量的修改后的值。
-
假设有一个线程A,有一个被关键字volatile修饰的变量i;只有当线程A对变量i执行的前一个操作是assign操作的时候,才能对其进行store操作;并且,只有线程A对变量i执行后一个操作是store操作的时候,线程A才能对变量i进行assign操作,也就是说,线程A对变量i执行assign操作是和对其执行store操作和write操作相关联的,它们都必须要连续一起出现。
这条规则要求在工作内存中,每次修改volatile变量时都要立刻同步回主内存,用于保证其他线程能看见volatile变量修改后的值。
-
假设有一个线程A,有两个被关键字volatile修饰的变量,分别为i和j;假定动作A是线程A对volatile变量i执行use操作或者assign操作,假定动作B是和动作A相关联的load操作或者store操作,假定动作C是和动作B相关联的read操作或者write操作;假定动作D是线程A对volatile变量j执行use操作或者assign操作,假定动作E是和动作D相关联的load操作或者store操作,假定动作F是和动作E相关联的read操作或者write操作;如果动作A先于动作D,那么动作C先于动作F。
这条规则要求被关键字volatile修饰的变量不会被指令重排序优化,保证了代码的执行顺序和程序的顺序相同。
题外话
HSDIS是一个Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件,它包含在HotSpot虚拟机的源码中,但是没有提供编译后的程序。
我讲解下如何用HSDIS插件查看类的的汇编代码,步骤如下:
-
下载hsdis-amd64.dylib,链接如下:
-
将hsdis-amd64.dylib放在目录:/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/lib
-
配置IntelliJ IDEA运行参数,打开界面,如下图所示:
ConfigureTheRunParametersInterface.png在VM options填上运行参数,如下图所示:
OperationParameters.png运行参数如下:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=compileonly,*Sinlgeton.getInstance -XX:+LogCompilation -XX:LogFile=SingletonAssemblyCodeWithVolatile.log
- -XX:+UnlockDiagnosticVMOptions:解锁用于JVM诊断的选项。
- -XX:+PrintAssembly:输出反汇编内容。
- -Xcomp:让虚拟机以编译模式执行代码,这样就可以不需要执行足够次数来预热就能触发JIT编译。
- -XX:CompileCommand=compileonly,Singleton.getInstance:让编译器不要内联getInstance方法并且只编译getInstance方法*。
- -XX:+LogCompilation:允许将汇编代码记录到当前工作目录名为hotspot.log文件中,可以通过-XX:LogFile指定文件名字和文件路径。
- -XX:LogFile=SingletonAssemblyCodeWithVolatile.log:指定汇编代码记录的文件名字和文件路径,这里指定文件名字是SingletonAssemblyCodeWithVolatile.log。
-
点run,然后就会在项目目录下生成SingletonAssemblyCodeWithVolatile.log文件,查看这个文件就可以看到汇编代码了。
我的GitHub:TanJiaJunBeyond
Android通用框架:Android通用框架
我的掘金:谭嘉俊
我的简书:谭嘉俊
我的CSDN:谭嘉俊
网友评论