并发编程中,最常用的两种机制:Synchronized和Volatile;Synchronized是共享资源在并发情况下常用方法来保证数据一致。Volatile是java虚拟机提供的最轻量级的同步机制主要适用于读多写少的场景,作用有一、保证共享变量可见性,二、内存屏障禁止指令重排序。
volatile可见性
说volatile可见性前,需要先了解java的内存模型,也就是JMM;
问题
如果多个线程同时去内存读取共享变量到各自的私有内存再做处理,那么多个线程彼此是不知道对共享变量做了什么操作的,可能就会导致一个线程覆盖了另一个线程的变更,从而产生线程安全问题。
解决方法
代码中可能会出现线程安全的共享变量,使用volatile修饰。如此,一个线程修改了这个变量就其他线程就会马上感知的到。
例子参考
public class LazySingleton {
private static volatile LazySingleton instance = null;
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
private LazySingleton(){}
public static void main(String[] args) {
LazySingleton.getInstance();
}
}
懒加载模式的单例创建代码
翻译为对应的字节码如下
1 Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
2 CompilerOracle: compileonly *LazySingleton.getInstance
3 Loaded disassembler from D:\JDK\jre\bin\server\hsdis-amd64.dll
4 Decoding compiled method 0x0000000002931150:
5 Code:
6 Argument 0 is unknown.RIP: 0x29312a0 Code size: 0x00000108
7 [Disassembling for mach='amd64']
8 [Entry Point]
9 [Verified Entry Point]
10 [Constants]
11 # {method} 'getInstance' '()Lorg/xrq/test/design/singleton/LazySingleton;' in 'org/xrq/test/design/singleton/LazySingleton'
12 # [sp+0x20] (sp of caller)
13 0x00000000029312a0: mov dword ptr [rsp+0ffffffffffffa000h],eax
14 0x00000000029312a7: push rbp
15 0x00000000029312a8: sub rsp,10h ;*synchronization entry
16 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@-1 (line 13)
17 0x00000000029312ac: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
18 0x00000000029312b6: mov r11d,dword ptr [r10+58h]
19 ;*getstatic instance
20 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@0 (line 13)
21 0x00000000029312ba: test r11d,r11d
22 0x00000000029312bd: je 29312e0h
23 0x00000000029312bf: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
24 0x00000000029312c9: mov r11d,dword ptr [r10+58h]
25 0x00000000029312cd: mov rax,r11
26 0x00000000029312d0: shl rax,3h ;*getstatic instance
27 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@16 (line 17)
28 0x00000000029312d4: add rsp,10h
29 0x00000000029312d8: pop rbp
30 0x00000000029312d9: test dword ptr [330000h],eax ; {poll_return}
31 0x00000000029312df: ret
32 0x00000000029312e0: mov rax,qword ptr [r15+60h]
33 0x00000000029312e4: mov r10,rax
34 0x00000000029312e7: add r10,10h
35 0x00000000029312eb: cmp r10,qword ptr [r15+70h]
36 0x00000000029312ef: jnb 293135bh
37 0x00000000029312f1: mov qword ptr [r15+60h],r10
38 0x00000000029312f5: prefetchnta byte ptr [r10+0c0h]
39 0x00000000029312fd: mov r11d,0e07d00b2h ; {oop('org/xrq/test/design/singleton/LazySingleton')}
40 0x0000000002931303: mov r10,qword ptr [r12+r11*8+0b0h]
41 0x000000000293130b: mov qword ptr [rax],r10
42 0x000000000293130e: mov dword ptr [rax+8h],0e07d00b2h
43 ; {oop('org/xrq/test/design/singleton/LazySingleton')}
44 0x0000000002931315: mov dword ptr [rax+0ch],r12d
45 0x0000000002931319: mov rbp,rax ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)
46 0x000000000293131c: mov rdx,rbp
47 0x000000000293131f: call 2907c60h ; OopMap{rbp=Oop off=132}
48 ;*invokespecial <init>
49 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@10 (line 14)
50 ; {optimized virtual_call}
51 0x0000000002931324: mov r10,rbp
52 0x0000000002931327: shr r10,3h
53 0x000000000293132b: mov r11,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
54 0x0000000002931335: mov dword ptr [r11+58h],r10d
55 0x0000000002931339: mov r10,7ada9e428h ; {oop(a 'java/lang/Class' = 'org/xrq/test/design/singleton/LazySingleton')}
56 0x0000000002931343: shr r10,9h
57 0x0000000002931347: mov r11d,20b2000h
58 0x000000000293134d: mov byte ptr [r11+r10],r12l
59 0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance
60 ; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)
61 0x0000000002931356: jmp 29312bfh
62 0x000000000293135b: mov rdx,703e80590h ; {oop('org/xrq/test/design/singleton/LazySingleton')}
63 0x0000000002931365: nop
64 0x0000000002931367: call 292fbe0h ; OopMap{off=204}
65 ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)
66 ; {runtime_call}
67 0x000000000293136c: jmp 2931319h
68 0x000000000293136e: mov rdx,rax
69 0x0000000002931371: jmp 2931376h
70 0x0000000002931373: mov rdx,rax ;*new ; - org.xrq.test.design.singleton.LazySingleton::getInstance@6 (line 14)
71 0x0000000002931376: add rsp,10h
72 0x000000000293137a: pop rbp
73 0x000000000293137b: jmp 2932b20h ; {runtime_call}
74 [Stub Code]
75 0x0000000002931380: mov rbx,0h ; {no_reloc}
76 0x000000000293138a: jmp 293138ah ; {runtime_call}
77 [Exception Handler]
78 0x000000000293138f: jmp 292fca0h ; {runtime_call}
79 [Deopt Handler Code]
80 0x0000000002931394: call 2931399h
81 0x0000000002931399: sub qword ptr [rsp],5h
82 0x000000000293139e: jmp 2909000h ; {runtime_call}
83 0x00000000029313a3: hlt
84 0x00000000029313a4: hlt
85 0x00000000029313a5: hlt
86 0x00000000029313a6: hlt
87 0x00000000029313a7: hlt
底层原理
java跨平台是怎么实现的呢?
首先需要有个常识:不论哪种语言最终需要转换为汇编语言才能被硬件平台给执行。比如C和C++都将我们的源代码直接编译为CPU相关的汇编指令给CPU执行。不同系列的CPU的体系架构也不相同,对应的汇编指令也不一样,比如X86架构的CPU对应X86的汇编指令,arm架构的CPU对应arm的汇编指令。
如果直接将源代码编译为与硬件相关的底层汇编指令,那么程序跨平台的特性就会大打折扣,不过执行效率较高。为了实现跨平台,java编译器javac将java源代码编译为字节码文件,也就是我们常见的class文件。字节码文件顾名思义里面是一个个的字节码,而class文件存的不是二进制,而是十六进制,因为二进制太长了,一个字节要8位二进制,所以用十六进制表示,两个十六进制就可以表示一个字节。
既然java源代码编译为了字节码文件不能被CPU执行,那么java是如何实现跨平台的呢,答案是JVM。为了让java可以跨平台执行,java官方提供了针对各个平台的java虚拟机,JVM运行于硬件层之上,屏蔽了各个平台的差异性。
javac编译后的字节码文件,统一由JVM来加载,最后再转换为与硬件相关的机器指令被CPU执行。
还有一个问题,JVM是如何将源代码和字节码文件中的字节码对应上的,即JVM如何知道哪段字节码是干什么用的。所以这就需要定义一个JVM层面上的规范,在JVM层面抽象出一些我们能够认识的指令助记符,这些指令助记符就是java的字节码指令。
在上面例子的字节码文件中,可以看到第59行指令前有lock指令,通过后面的注释我们知道是volatile修饰的instance在被写的时候加上的。
lock指令在多核处理器下会引发下面的操作:
1、将当前处理器的缓存行的数据写回到系统内存。
2、使其他处理器里缓存了改内存地址的数据置为无效
lock指令底层实现
为了提高处理器速度,cpu一般不直接跟内存进行通信而是将内存的数据读到cpu内部缓存后再进行操作,可是操作完成后cpu并不知道何时将缓存数据回写到内存。
可如果变量被volatile修饰后,当cpu对这个变量做完写操作后,JVM就会向cpu发送一条lock前缀指令,就会将这个变量值的缓存行数据回写到内存。
这时变量值虽然写回到了内存,可其他处理器还是缓存的旧值,还需要将其他处理器的缓存值变为内存的最新值。此时就需要多处理器实现缓存一致性协议(MESI协议)。即在一个处理器将自己缓存的数据写回到内存后,其他处理器就会通过嗅探在总线上传播的数据来检查自己缓存的数据是否过期,当处理器发现自己缓存对应的内存地址上的数据被修改后,就会将自己缓存行缓存的数据设置为无效,当处理器对这个变量进行操作的时候重新从系统内存中把数据读取到自己的缓存行,重新缓存。
嗅探机制
1、所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,
但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。
CPU缓存不仅仅在做内存传输的时候才与总线打交道,
而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。
所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。
只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。
volatile有序性
volatile通过内存屏障来保证指令不重排序即指令的有序性。
内存屏障分为两种:Load Barrier 和 Store Barrier,即读屏障和写屏障
内存屏障有两个作用:
1、阻止屏障两侧的指令重排序
2、强制把写缓存区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效
对于Load barrier来说,在指令前插入Load barrier,可以让cpu的缓存中数据失效,强制重新从内存中加载数据。(强制自己重读)
对于Store barrier来说,在指令后插入Store barrier,能让写入缓存中的最新数据更新到内存,让其他线程可见。(强制其他重读)
java内存屏障通常有四种:
LoadLoad、StoreStore、LoadStore、StoreLoad
实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
1、LoadLoad屏障:
Load1;LoadLoad;Load2;
在Load2及后续读操作读取数据前,保证Load1要读取的数据先被读取完。
2、StoreStore屏障:
Store1;StoreStore;Store2;
在store2及后续的写操作执行前,保证store1的写入操作对其他处理器可见。
3、LoadStore屏障:
Load1;LoadStore;Store2;
store2及后续写入操作被刷出前,保证load1要读取完毕。
4、StoreLoad屏障:
Store1;StoreLoad;Load2;
在load2及后续所有读操作执行前,保证store1的写入操作对其他处理器可见。
它是四种屏障中开销最大的,在大多数处理器实现中,这个屏障是个万能屏障,
兼具其他三种屏障的功能。
volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
在每个volatile写操作前插入StoreStore屏障;
在每个volatile写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障;
在每个volatile读操作后插入LoadStore屏障;