zoukankan      html  css  js  c++  java
  • 从 DCL(双重检查锁定)谈 volatile 禁止指令重排序

    引言

    最近在看《Java并发编程的艺术》,看到双重检查锁定里谈到用 volatile 来解决创建对象时,指令重排序的问题,想了解清楚为什么 volatile 可以禁止指令重排序,结果得到了出乎意料的答案。

    DCL(双重检查锁定)里发现的东西

    下面是使用 volatile 来优化双重检查锁定的代码:

    public class SafeDoubleCheckedLocking { 
    	private volatile static Instance instance; 
    	public static Instance getInstance() { 
    		if (instance == null) { 
    			synchronized (SafeDoubleCheckedLocking.class) { 
    				if (instance == null) 
    					instance = new Instance();
    			}
    		} 
    		return instance;
    	}
    }
    

    关于双重锁定的相关知识不在这里展开了,我们要关注的是

    private volatile static Instance instance;
    

    以及

    instance = new Instance();
    

    instance = new Instance();创建了一个新对象,这一 行代码可以分解为如下的3行伪代码。

    memory = allocate();  // 1:分配对象的内存空间 
    ctorInstance(memory); // 2:初始化对象 
    instance = memory;    // 3:设置instance指向刚分配的内存地址
    

    我们知道,编辑器和处理器会进行代码优化,而其中重要的一点是会将指令进行重排序。
    上边的代码经过重排序后可能会变为

    memory = allocate();  // 1:分配对象的内存空间  
    instance = memory;    // 3:设置instance指向刚分配的内存地址
    					  // 注意:此时对象尚未初始化
    ctorInstance(memory); // 2:初始化对象
    

    这样会引起一些问题,打个比方,有一个线程A在创建对象,另一个线程B判断对象是否为空if (instance == null),如果指令被重排序,那么当A尚未初始化对象但已分配内存地址时,若B在做判断,会得到错误的结果。
    所以,用 volatile 来禁止上述指令的重排序,使B的判断不会出错。
    private volatile static Instance instance;
    那这是怎么做到的呢,还是令人疑惑,我们继续探究。

    volatile 的特性

    先来看 valatile 的特性,可以看到并没有禁止指令重排序的相关特性。

    1. 可见性
      对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
    2. 原子性
      对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。
      不过,从上边的第一条特性可见性中,最后的,这三个字,我们其实已经能解决疑惑中的一部分,我们可以得到用 volatile ,使B的判断不会出错 ,B是一定在A创建对象完毕之后才能进行判断。那么还剩下一点volatile 来禁止上述指令的重排序

    volatile 变量的汇编代码

    这个时候,那么我们来看看在JIT汇编代码中mInstance = new Singleton()是怎样执行的:

    0x01a3de0f:mov		$0x3375cdb0,%esi		;……beb0cd75 33
    											;{oop('Singleton')}
    0x01a3de14:mov		%eax,0x150(%esi)		;……89865001 0000
    0x01a3de1a:shr		$0x9,%esi				;……c1ee09
    0x01a3de1d:movb	$0x0,0x1104800(%esi)	;……c6860048 100100
    0x01a3de24:lock	addl$0x0,(%esp)		;……f0830424 00
    											;*put static instance
    											;-
    Singleton:getInstance@24
    

    生成汇编码是lock addl $0x0, (%rsp), 在写操作(put static instance)之前使用了lock前缀,锁住了总线和对应的地址,这样其他的CPU写和读都要等待锁的释放。当写完成后,释放锁,把缓存刷新到主内存。
    加了 volatile之后,volatile在最后加了lock前缀,把前面的步骤锁住了,这样如果你前面的步骤没做完是无法执行最后一步刷新到内存的,换句话说只要执行到最后一步lock,必定前面的操作都完成了。那么即使我们完成前面两步或者三步了,还没执行最后一步lock,或者前面一步执行了就切换线程2了,线程B在判断的时候也会判断实例为空,进而继续进来由线程B完成后面的所有操作。当写完成后,释放锁,把缓存刷新到主内存。

    注意

    这里我们就可以看到此内存屏障只保证lock前后的顺序不颠倒,但是并没有保证前面的所有顺序都是要顺序执行的,比如我有1 2 3 4 5 6 7步,而lock在4步,那么前面123是可以乱序的,只要123乱序执行的结果和顺序执行是一样的,后面的567也是一样可以乱序的,但是整体上我们是顺序的,把123看成一个整体,4是一个整体 567又是一个整体,所以整体上我们的顺序的执行的,也达到了看起来禁止重排的效果

    结论

    volatile 没有禁止指令重排序,它只是在创建变量的过程中上锁,如果一个线程A在创建变量,另一个线程B尝试读取变量,那么,在A创建完毕之前,B是读不到变量的,这就避免了错误。
    所以,我得到了一个我也不敢确定的结论:

    书上错了,volatile 没有禁止指令重排序
    

    就在《Java并发编程的艺术》的 3.8.3 小节:
    当声明对象的引用为volatile后,3.8.2节中的3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止。
    如果哪位能解答一下我的疑惑,就再好不过了。

    参考

    1. 《Java并发编程的艺术》——方腾飞 魏鹏 程晓明 著
    2. Volatile 以DCL失效谈内存屏障用来禁止指令重排序的原理——HJsirVolatile 以DCL失效谈内存屏障用来禁止指令重排序的原理——HJsir
    3. volatile变量详解——栋先生
  • 相关阅读:
    多线程面试题
    Tcpdump MySQL Query
    Gossip和Redis集群原理
    mysql-table_open_cache_file_limits/
    introducing-backup-locks-percona-server-2/
    MySQL 一致性读 深入研究
    how-to-configure-mysql-masterslave-replication-with-mha-automatic-failover/
    mysqlOOM
    mysql 线程池
    Linux performance monitor tool
  • 原文地址:https://www.cnblogs.com/Sherlock-J/p/12925940.html
Copyright © 2011-2022 走看看