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:

