java 中volatile和lock原理分析
volatile和lock是Java中用于线程协同同步的两种机制。
Volatile
volatile是Java中的一个关键字,它的作用有
- 保证变量的可见性
- 防止重排序
- 保证64位变量(long,double)的原子性读写
volatile在Java语言规范中规定的是
The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables. The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable . It is a compile-time error if a final variable is also declared volatile.</div>
Java内存模型中规定了volatile的happen-before效果,对volatile变量的写操作happen-before于后续的读。这样volatile变量能够确保一个线程的修改对其他线程可见。volatile因为不能保证原子性,所以不能在有约束或后验条件的场景下使用,如i++,常用的场景是stop变量保证系统停止对其他线程可见,double-check lock单例中防止重排序来保证安全发布等。
以下面这段代码为例
public class TestVolatile { private static volatile boolean stop = false; public static void main(String[] args) { stop = true; boolean b = stop; } }</div>
stop字段声明为volatile类型后,编译后的字节码中其变量的access_flag中ACC_VOLATILE位置为1。
关键的字节码内容如下
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=1 0: iconst_1 1: putstatic #2 // Field stop:Z 4: getstatic #2 // Field stop:Z 7: istore_1 8: return LineNumberTable: line 14: 0 line 15: 4 line 16: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; 8 1 1 b Z static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_0 1: putstatic #2 // Field stop:Z 4: return LineNumberTable: line 11: 0 }</div>
通过hsdis查看虚拟机产生的汇编代码。
测试环境为java version “1.8.0_45”,MACOS10.12.1 i386:x86-64
在执行参数上添加
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*TestVolatile.main -XX:CompileCommand=compileonly,*TestVolatile.main</div>
查看main方法的汇编指令结果
Decoding compiled method 0x000000010c732c50: Code: [Disassembling for mach='i386:x86-64'] [Entry Point] [Verified Entry Point] [Constants] # {method} {0x000000012422a2c8} 'main' '([Ljava/lang/String;)V' in 'com/concurrent/volatiles/TestVolatile' # parm0: rsi:rsi = '[Ljava/lang/String;' # [sp+0x40] (sp of caller) 0x000000010c732da0: mov %eax,-0x14000(%rsp) 0x000000010c732da7: push %rbp 0x000000010c732da8: sub $0x30,%rsp 0x000000010c732dac: movabs $0x12422a448,%rdi ; {metadata(method data for {method} {0x000000012422a2c8} 'main' '([Ljava/lang/String;)V' in 'com/concurrent/volatiles/TestVolatile')} 0x000000010c732db6: mov 0xdc(%rdi),%ebx 0x000000010c732dbc: add $0x8,%ebx 0x000000010c732dbf: mov %ebx,0xdc(%rdi) 0x000000010c732dc5: movabs $0x12422a2c8,%rdi ; {metadata({method} {0x000000012422a2c8} 'main' '([Ljava/lang/String;)V' in 'com/concurrent/volatiles/TestVolatile')} 0x000000010c732dcf: and $0x0,%ebx 0x000000010c732dd2: cmp $0x0,%ebx 0x000000010c732dd5: je 0x000000010c732e03 ;*iconst_1 ; - com.concurrent.volatiles.TestVolatile::main@0 (line 14) 0x000000010c732ddb: movabs $0x76adce798,%rsi ; {oop(a 'java/lang/Class' = 'com/concurrent/volatiles/TestVolatile')} 0x000000010c732de5: mov $0x1,%edi 0x000000010c732dea: mov %dil,0x68(%rsi) 0x000000010c732dee: lock addl $0x0,(%rsp) ;*putstatic stop ; - com.concurrent.volatiles.TestVolatile::main@1 (line 14) 0x000000010c732df3: movsbl 0x68(%rsi),%esi ;*getstatic stop ; - com.concurrent.volatiles.TestVolatile::main@4 (line 15) 0x000000010c732df7: add $0x30,%rsp 0x000000010c732dfb: pop %rbp 0x000000010c732dfc: test %eax,-0x3adbd02(%rip) # 0x0000000108c57100 ; {poll_return} 0x000000010c732e02: retq 0x000000010c732e03: mov %rdi,0x8(%rsp) 0x000000010c732e08: movq $0xffffffffffffffff,(%rsp) 0x000000010c732e10: callq 0x000000010c7267e0 ; OopMap{rsi=Oop off=117} ;*synchronization entry ; - com.concurrent.volatiles.TestVolatile::main@-1 (line 14) ; {runtime_call} 0x000000010c732e15: jmp 0x000000010c732ddb 0x000000010c732e17: nop 0x000000010c732e18: nop 0x000000010c732e19: mov 0x2a8(%r15),%rax 0x000000010c732e20: movabs $0x0,%r10 0x000000010c732e2a: mov %r10,0x2a8(%r15) 0x000000010c732e31: movabs $0x0,%r10 0x000000010c732e3b: mov %r10,0x2b0(%r15) 0x000000010c732e42: add $0x30,%rsp 0x000000010c732e46: pop %rbp 0x000000010c732e47: jmpq 0x000000010c6940e0 ; {runtime_call} [Exception Handler]</div>
可以看到在mov %dil,0x68(%rsi)给stop赋值后增加了lock addl $0x0,(%rsp)
IA32中对lock的说明是
The LOCK # signal is asserted during execution of the instruction following the lock prefix. This signal can be used in a multiprocessor system to ensure exclusive use of shared memory while LOCK # is asserted</div>
lock用于在多处理器中执行指令时对共享内存的独占使用。它的副作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。另外还提供了有序的指令无法越过这个内存屏障的作用。
Lock
Java中提供的锁的关键字是synchronized, 可以加在方法块上,也可以加在方法声明中。
synchronized关键字起到的作用是设置一个独占访问临界区,在进入这个临界区前要先获取对应的监视器锁,任何Java对象都可以成为监视器锁,声明在静态方法上时监视器锁是当前类的Class对象,实例方法上是当前实例。
synchronized提供了原子性、可见性和防止重排序的保证。
JMM中定义监视器锁的释放操作happen-before与后续的同一个监视器锁获取操作。再结合程序顺序规则就可以形成内存传递可见性保证。
下面以一段代码查看各个层次的实现
public class TestSynchronize { private int count; private void inc() { synchronized (this) { count++; } } public static void main(String[] args) { new TestSynchronize().inc(); } }</div>
编译后inc方法的字节码为
private void inc(); descriptor: ()V flags: ACC_PRIVATE Code: stack=3, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_0 5: dup 6: getfield #2 // Field count:I 9: iconst_1 10: iadd 11: putfield #2 // Field count:I 14: aload_1 15: monitorexit 16: goto 24 19: astore_2 20: aload_1 21: monitorexit 22: aload_2 23: