上两篇文章中主要讲解了synchronized和volatile关键字涉及的底层设计及相关概念,同时也包括了synchronized锁升级过程。本文中将重点讲解volatile的底层实现及DCL单例是否需要加volatile修饰。
volatile
volatile作用
volatile主要有两个作用:
- 内存可见性
- 禁止指令重排序
内存可见性:简单来说就是两个线程间公用一个变量,当一个线程修改了这个变量,修改后对另一个线程可见。
禁止指令重排序:cpu在执行代码指令时为了提高性能,会乱序执行指令,会导致在多线程的情况下,出现最终执行结果不一致。为了解决这个问题,可以使用volatile关键字进行修复。
volatile实现机制
volatile内存可见性在hotsport虚拟机中是通过lock addl指令来实现的,也就是说jvm会将lock addl指令发送给当前处理器,操作系统会强制将最新变量值刷新至主内存,并将其他处理器该变量的缓存行置为无效,其利用了操作系统的缓存一致性(MESI)协议。
volatile禁止指令重排序是由内存屏障控制的,具体可以看上一篇中关于内存屏障的讲解。
看下面的一个实例:
public class DCL {
private static volatile DCL DCL = null;
public int a = 1;
static Object o = new Object();
private DCL() {
}
public static DCL newInstance() {
if (DCL == null) {
synchronized (o) {
if (DCL == null) {
DCL = new DCL();
}
}
}
return DCL;
}
}
上面的实例中DCL dcl 变量由volatile修饰,我看先看下其关键的字节码指令片段:
能看到volatile修饰的变量和普通的变量字节码的区别是加了ACC_VOLATILE指令,下面我们来看下上面代码对应的汇编指令码是什么样的,查看汇编指令码,如何查看汇编码呢?
- 需要用hsdis-amd64.dll后(自己没有的给我留言我发给您),将 hsdis-amd64.dylib 放到%JAVA_HOME%/jre/lib目录下。
- 在jvm启动参数中加入:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline
- 运行程序,就会输出对应的汇编码。
- 网上只有linux、mac等系统的dll,windows系统需要自己编译才行。
...
[Constants]
# {method} {0x000000001bef2c80} '<clinit>' '()V' in 'com/cacheline/DCL'
# [sp+0x40] (sp of caller)
0x0000000002dafe00: mov %eax,-0x6000(%rsp)
0x0000000002dafe07: push %rbp
0x0000000002dafe08: sub $0x30,%rsp
0x0000000002dafe0c: movabs $0x1bef2da0,%rdx ; {metadata(method data for {method} {0x000000001bef2c80} '<clinit>' '()V' in 'com/cacheline/DCL')}
0x0000000002dafe16: mov 0xdc(%rdx),%esi
0x0000000002dafe1c: add $0x8,%esi
0x0000000002dafe1f: mov %esi,0xdc(%rdx)
0x0000000002dafe25: movabs $0x1bef2c78,%rdx ; {metadata({method} {0x000000001bef2c80} '<clinit>' '()V' in 'com/cacheline/DCL')}
0x0000000002dafe2f: and $0x0,%esi
0x0000000002dafe32: cmp $0x0,%esi
0x0000000002dafe35: je 0x0000000002daff29 ;*aconst_null
; - com.cacheline.DCL::<clinit>@0 (line 9)
0x0000000002dafe3b: nopl 0x0(%rax,%rax,1)
0x0000000002dafe40: jmpq 0x0000000002daff95 ; {no_reloc}
0x0000000002dafe45: add %al,(%rax)
0x0000000002dafe47: add %al,(%rax)
0x0000000002dafe49: add %cl,-0x42(%rax) ; {oop(NULL)}
0x0000000002dafe4c: add %al,(%rax)
0x0000000002dafe4e: add %al,(%rax)
0x0000000002dafe50: add %al,(%rax)
0x0000000002dafe52: add %al,(%rax)
0x0000000002dafe54: mov %rsi,%r10
0x0000000002dafe57: shr $0x3,%r10
0x0000000002dafe5b: mov %r10d,0x68(%rdx)
0x0000000002dafe5f: shr $0x9,%rdx
0x0000000002dafe63: movabs $0xe7a5000,%rbx
0x0000000002dafe6d: movb $0x0,(%rdx,%rbx,1)
0x0000000002dafe71: lock addl $0x0,(%rsp) ;*putstatic dcl
; - com.cacheline.DCL::<clinit>@1 (line 9)
上面是DCL.java对应的汇编码片段(汇编码非常长,只能截取片段),最后一行0x0000000002dafe71: lock addl $0x0,(%rsp) ;*putstatic dcl,可以看到dcl实例加了volatile修饰后,在操作系统层面会加lock addl指令进行修饰,也就是在os层面volatile是通过lock addl来实现的。
DCL单例是否需要volatile修饰
上一部分的DCL.java代码即是DCL单例的代码,这里不在重复,看上面的java代码即可。
我们需要知道对象的创建过程:
new
invokespecial #1 // Method java/lang/Object.""😦)V
astore_1
new:在内存中申请空间
invokespecial:执行构造方法,初始化对象,a=1
astore_1:建立对象引用,
也就是说如果我们不给对象加volatile修饰,那么指令完全可能变为:
new
astore_1
invokespecial #1 // Method java/lang/Object.""😦)V
也就是先建立对象引用,后初始化对象。
这样DCL单例就有问题了。当=线程1new DCL()时,cpu指令乱序执行,先执行了astore_1,完成了对象的引用,此时正巧线程2执行newInstance方法,判断dcl对象不为空,那么就直接返回了DCL类的实例,但是这样是有问题的,因为此时DCL对象是半初始化状态,即变量a的值还是等于0,那么线程2此时使用DCL对象就可能发生问题。故DCL单例需要加volatile修饰。
最后
本文主要讲解了volatile的底层实现及DCL单例是否需要加volatile修饰。还告诉大家怎么打印汇编码指令。如果本篇对你有用,欢迎点赞、关注、转载,由于水平有限,如有问题请留言。