从kernel层面分析synchronized、volatile,进大厂必备硬核小伎俩(下)

上两篇文章中主要讲解了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字节码
能看到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修饰。还告诉大家怎么打印汇编码指令。如果本篇对你有用,欢迎点赞、关注、转载,由于水平有限,如有问题请留言。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值