精华内容
下载资源
问答
  • JUC 指令重排与内存屏障
    2021-03-16 03:35:49

    指令重排

    对于编译器的编写者来说,Java内存模型(JMM)主要是由禁止指令重排的规则所组成的,其中包括了字段(包括数组中的元素)的存取指令和监视器(锁)的控制指令。

    Volatile与监视器

    JMM中关于volatile和监视器主要的规则可以被看作一个矩阵。这个矩阵的单元格表示在一些特定的后续关联指令的情况下,指令不能被重排。下面的表格并不是JMM规范包含的,而是一个用来观察JMM模型对编译器和运行系统造成的主要影响的工具。

    能否重排

    第二个操作

    第一个操作

    Normal Load Normal Store

    Volatile load MonitorEnter

    Volatile store MonitorExit

    Normal Load Normal Store

    No

    Volatile load MonitorEnter

    No

    No

    No

    Volatile store MonitorExit

    No

    No

    关于上面这个表格一些术语的说明:

    Normal Load指令包括:对非volatile字段的读取,getfield,getstatic和array load;

    Normal Store指令包括:对非volatile字段的存储,putfield,putstatic和array store;

    Volatile load指令包括:对多线程环境的volatile变量的读取,getfield,getstatic;

    Volatile store指令包括:对多线程环境的volatile变量的存储,putfield,putstatic;

    MonitorEnters指令(包括进入同步块synchronized方法)是用于多线程环境的锁对象;

    MonitorExits指令(包括离开同步块synchronized方法)是用于多线程环境的锁对象。

    在JMM中,Normal Load指令与Normal store指令的规则是一致的,类似的还有Volatile load指令与MonitorEnter指令,以及Volatile store指令与MonitorExit指令,因此这几对指令的单元格在上面表格里都合并在了一起(但是在后面部分的表格中,会在有需要的时候展开)。在这个小节中,我们仅仅考虑那些被当作原子单元的可读可写的变量,也就是说那些没有位域(bit fields),非对齐访问(unaligned accesses)或者超过平台最大字长(word size)的访问。

    任意数量的指令操作都可被表示成这个表格中的第一个操作或者第二个操作。例如在单元格[Normal Store, Volatile Store]中,有一个No,就表示任何非volatile字段的store指令操作不能与后面任何一个Volatile store指令重排, 如果出现任何这样的重排会使多线程程序的运行发生变化。

    JSR-133规范规定上述关于volatile和监视器的规则仅仅适用于可能会被多线程访问的变量或对象。因此,如果一个编译器可以最终证明(往往是需要很大的努力)一个锁只被单线程访问,那么这个锁就可以被去除。与之类似的,一个volatile变量只被单线程访问也可以被当作是普通的变量。还有进一步更细粒度的分析与优化,例如:那些被证明在一段时间内对多线程不可访问的字段。

    在上表中,空白的单元格代表在不违反Java的基本语义下的重排是允许的(详细可参考JLS中的说明)。例如,即使上表中没有说明,但是也不能对同一个内存地址上的load指令和之后紧跟着的store指令进行重排。但是你可以对两个不同的内存地址上的load和store指令进行重排,而且往往在很多编译器转换和优化中会这么做。这其中就包括了一些往往不认为是指令重排的例子,例如:重用一个基于已经加载的字段的计算后的值而不是像一次指令重排那样去重新加载并且重新计算。然而,JMM规范允许编译器经过一些转换后消除这些可以避免的依赖,使其可以支持指令重排。

    在任何的情况下,即使是程序员错误的使用了同步读取,指令重排的结果也必须达到最基本的Java安全要求。所有的显式字段都必须不是被设定成0或null这样的预构造值,就是被其他线程设值。这通常必须把所有存储在堆内存里的对象在其被构造函数使用前进行归零操作,并且从来不对归零store指令进行重排。一种比较好的方式是在垃圾回收中对回收的内存进行归零操作。可以参考JSR-133规范中其他情况下的一些关于安全保证的规则。

    这里描述的规则和属性都是适用于读取Java环境中的字段。在实际的应用中,这些都可能会另外与读取内部的一些记账字段和数据交互,例如对象头,GC表和动态生成的代码。

    Final 字段

    Final字段的load和store指令相对于有锁的或者volatile字段来说,就跟Normal load和Normal store的存取是一样的,但是需要加入两条附加的指令重排规则:

    如果在构造函数中有一条final字段的store指令,同时这个字段是一个引用,那么它将不能与构造函数外后续可以让持有这个final字段的对象被其他线程访问的指令重排。例如:你不能重排下列语句:

    x.finalField = v;

    ... ;

    sharedRef = x;

    这条规则会在下列情况下生效,例如当你内联一个构造函数时,正如“…”的部分表示这个构造函数的逻辑边界那样。你不能把这个构造函数中的对于这个final字段的store指令移动到构造函数外的一条store指令后面,因为这可能会使这个对象对其他线程可见。(正如你将在下面看到的,这样的操作可能还需要声明一个内存屏障)。类似的,你不能把下面的前两条指令与第三条指令进行重排:

    x.afield = 1; x.finalField = v; ... ; sharedRef = x;

    一个final字段的初始化load指令不能与包含该字段的对象的初始化load指令进行重排。在下面这种情况下,这条规则就会生效:x = shareRef; … ; i = x.finalField;

    由于这两条指令是依赖的,编译器将不会对这样的指令进行重排。但是,这条规则会对某些处理器有影响。

    上述规则,要求对于带有final字段的对象的load本身是synchronized,volatile,final或者来自类似的load指令,从而确保java程序员对与final字段的正确使用,并最终使构造函数中初始化的store指令和构造函数外的store指令排序。

    内存屏障

    编译器和处理器必须同时遵守重排规则。由于单核处理器能确保与“顺序执行”相同的一致性,所以在单核处理器上并不需要专门做什么处理,就可以保证正确的执行顺序。但在多核处理器上通常需要使用内存屏障指令来确保这种一致性。即使编译器优化掉了一个字段访问(例如,因为一个读入的值未被使用),这种情况下还是需要产生内存屏障,就好像这个访问仍然需要保护。(可以参考下面的优化掉内存屏障的章节)。

    内存屏障仅仅与内存模型中“获取”、“释放”这些高层次概念有间接的关系。内存屏障并不是“同步屏障”,内存屏障也与在一些垃圾回收机制中“写屏障(write barriers)”的概念无关。内存屏障指令仅仅直接控制CPU与其缓存之间,CPU与其准备将数据写入主存或者写入等待读取、预测指令执行的缓冲中的写缓冲之间的相互操作。这些操作可能导致缓冲、主内存和其他处理器做进一步的交互。但在JAVA内存模型规范中,没有强制处理器之间的交互方式,只要数据最终变为全局可用,就是说在所有处理器中可见,并当这些数据可见时可以获取它们。

    内存屏障的种类

    几乎所有的处理器至少支持一种粗粒度的屏障指令,通常被称为“栅栏(Fence)”,它保证在栅栏前初始化的load和store指令,能够严格有序的在栅栏后的load和store指令之前执行。无论在何种处理器上,这几乎都是最耗时的操作之一(与原子指令差不多,甚至更消耗资源),所以大部分处理器支持更细粒度的屏障指令。

    内存屏障的一个特性是将它们运用于内存之间的访问。尽管在一些处理器上有一些名为屏障的指令,但是正确的/最好的屏障使用取决于内存访问的类型。下面是一些屏障指令的通常分类,正好它们可以对应上常用处理器上的特定指令(有时这些指令不会导致操作)。

    LoadLoad 屏障

    序列:Load1,Loadload,Load2

    确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。

    StoreStore 屏障

    序列:Store1,StoreStore,Store2

    确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。

    LoadStore 屏障

    序列: Load1; LoadStore; Store2

    确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。

    StoreLoad Barriers

    序列: Store1; StoreLoad; Load2

    确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。

    在下面讨论的所有处理器中,执行StoreLoad的指令也会同时获得其他三种屏障的效果。所以StoreLoad可以作为最通用的(但通常也是最耗性能)的一种Fence。(这是经验得出的结论,并不是必然)。反之不成立,为了达到StoreLoad的效果而组合使用其他屏障并不常见。

    下表显示这些屏障如何符合JSR-133排序规则。

    需要的屏障

    第二步

    第一步

    Normal Load

    Normal Store

    Volatile Load MonitorEnter

    Volatile Store MonitorExit

    Normal Load

    LoadStore

    Normal Store

    StoreStore

    Volatile Load MonitorEnter

    LoadLoad

    LoadStore

    LoadLoad

    LoadStore

    Volatile Store MonitorExit

    StoreLoad

    StoreStore

    另外,特殊的final字段规则在下列代码中需要一个StoreStore屏障

    x.finalField = v; StoreStore; sharedRef = x;

    如下例子解释如何放置屏障:

    class X {

    int a, b;

    volatile int v, u;

    void f() {

    int i, j;

    i = a;// load a

    j = b;// load b

    i = v;// load v

    // LoadLoad

    j = u;// load u

    // LoadStore

    a = i;// store a

    b = j;// store b

    // StoreStore

    v = i;// store v

    // StoreStore

    u = j;// store u

    // StoreLoad

    i = u;// load u

    // LoadLoad

    // LoadStore

    j = b;// load b

    a = i;// store a

    }

    }

    数据依赖和屏障

    一些处理器为了保证依赖指令的交互次序需要使用LoadLoad和LoadStore屏障。在一些(大部分)处理器中,一个load指令或者一个依赖于之前加载值的store指令被处理器排序,并不需要一个显式的屏障。这通常发生于两种情况,间接取值(indirection):

    Load x; Load x.field

    和条件控制(control)

    Load x; if (predicate(x)) Load or Store y;

    但特别的是不遵循间接排序的处理器,需要为final字段设置屏障,使它能通过共享引用访问最初的引用。

    x = sharedRef; … ; LoadLoad; i = x.finalField;

    相反的,如下讨论,确定遵循数据依赖的处理器,提供了几个优化掉LoadLoad和LoadStore屏障指令的机会。(尽管如此,在任何处理器上,对于StoreLoad屏障不会自动清除依赖关系)。

    与原子指令交互

    屏障在不同处理器上还需要与MonitorEnter和MonitorExit实现交互。锁或者解锁通常必须使用原子条件更新操作CompareAndSwap(CAS)指令或者LoadLinked/StoreConditional (LL/SC),就如执行一个volatile store之后紧跟volatile load的语义一样。CAS或者LL/SC能够满足最小功能,一些处理器还提供其他的原子操作(如,一个无条件交换),这在某些时候它可以替代或者与原子条件更新操作结合使用。

    在所有处理器中,原子操作可以避免在正被读取/更新的内存位置进行写后读(read-after-write)。(否则标准的循环直到成功的结构体(loop-until-success )没有办法正常工作)。但处理器在是否为原子操作提供比隐式的StoreLoad更一般的屏障特性上表现不同。一些处理器上这些指令可以为MonitorEnter/Exit原生的生成屏障;其它的处理器中一部分或者全部屏障必须显式的指定。

    为了分清这些影响,我们必须把Volatiles和Monitors分开:

    需要的屏障

    第二步

    第一步

    Normal Load

    Normal Store

    Volatile Load

    Volatile Store

    MonitorEnter

    MonitorExit

    Normal Load

    LoadStore

    LoadStore

    Normal Store

    StoreStore

    StoreExit

    Volatile Load

    LoadLoad

    LoadStore

    LoadLoad

    LoadStore

    LoadEnter

    LoadExit

    Volatile Store

    StoreLoad

    StoreStore

    StoreEnter

    StoreExit

    MonitorEnter

    EnterLoad

    EnterStore

    EnterLoad

    EnterStore

    EnterEnter

    EnterExit

    MonitorExit

    ExitLoad

    ExitStore

    ExitEnter

    ExitExit

    另外,特殊的final字段规则需要一个StoreLoad屏障。

    x.finalField = v; StoreStore; sharedRef = x;

    在这张表里,”Enter”与”Load”相同,”Exit”与”Store”相同,除非被原子指令的使用和特性覆盖。特别是:

    EnterLoad 在进入任何需要执行Load指令的同步块/方法时都需要。这与LoadLoad相同,除非在MonitorEnter时候使用了原子指令并且它本身提供一个至少有LoadLoad属性的屏障,如果是这种情况,相当于没有操作。

    StoreExit在退出任何执行store指令的同步方法块时候都需要。这与StoreStore一致,除非MonitorExit使用原子操作,并且提供了一个至少有StoreStore属性的屏障,如果是这种情况,相当于没有操作。

    ExitEnter和StoreLoad一样,除非MonitorExit使用了原子指令,并且/或者MonitorEnter至少提供一种屏障,该屏障具有StoreLoad的属性,如果是这种情况,相当于没有操作。

    在编译时不起作用或者导致处理器上不产生操作的指令比较特殊。例如,当没有交替的load和store指令时,EnterEnter用于分离嵌套的MonitorEnter。下面这个例子说明如何使用这些指令类型:

    class X {

    int a;

    volatile int v;

    void f() {

    int i;

    synchronized (this) { // enter EnterLoad EnterStore

    i = a;// load a

    a = i;// store a

    }// LoadExit StoreExit exit ExitEnter

    synchronized (this) {// enter ExitEnter

    synchronized (this) {// enter

    }// EnterExit exit

    }// ExitExit exit ExitEnter ExitLoad

    i = v;// load v

    synchronized (this) {// LoadEnter enter

    } // exit ExitEnter ExitStore

    v = i; // store v

    synchronized (this) { // StoreEnter enter

    } // EnterExit exit

    }

    }

    Java层次的对原子条件更新的操作将在JDK1.5中发布(JSR-166),因此编译器需要发布相应的代码,综合使用上表中使用MonitorEnter和MonitorExit的方式,——从语义上说,有时在实践中,这些Java中的原子更新操作,就如同他们都被锁所包围一样。

    更多相关内容
  • volatile 的效果2.1 内存可见性2.2 内存屏障3. volatile 的用途3.1 单例模式双重检查锁 一、volatile 关键字 1. 几个基本概念 1.1 内存可见性 我们都知道,在Java内存模型中,JMM 中有一个内存,每个线程有自己独立...


    深入理解 synchronized 关键字(关于对象头和锁的升级)

    一、volatile 关键字

    1. 几个基本概念

    1.1 内存可见性

    我们都知道,在Java内存模型中,JMM 中有一个内存,每个线程有自己独立的内存空间,线程的私有内存中包含了对内存中的变量的拷贝。

    内存可见性就是指当两个线程的私有内存中有对内存中同一个变量的拷贝时,当其中一个线程修改这个变量,另一个线程是否马上就能感应到这个值

    1.2 指令重排序

    我们知道在一些时候,如果几条语句不具有关联性,编译器或者CPU为了提高性能,会对代码语句进行重排序,这样做是为了更加有效的利用CPU 的资源(流水线的思想)

    1.3 happens-before 规则

    重排序需要遵守 happens-before 规则,不能说你想怎么排就怎么排,如果那样岂不是乱了套。

    1、程序顺序规则:一个线程中的每个操作 happens-before 于该线程中的任意后续操作

    2、监视器锁(同步)规则:对于一个监视器的解锁,happens-before于随后对这个监视器的加锁

    程序顺序规则中所说的每个操作happens-before于该线程中的任意后续操作并不是说前一个操作必须要在后一个操作之前执行,而是指前一个操作的执行结果必须对后一个操作可见,如果不满足这个要求那就不允许这两个操作进行重排序。

    例如

    
    public double rectangleArea(double length , double width){
    	
    	double leng;
    	double wid;
    	leng=length;//A	
    	wid=width;//B
    	double area=leng*wid;//C
    	return area;
    	
    }
    

    因为A happens-before B,所以A操作产生的结果leng一定要对B操作可见,但是现在B操作并没有用到length,所以这两个操作可以重排序,那A操作是否可以和C操作重排序呢,如果A操作和C操作进行了重排序,因为leng没有被赋值,所以leng=0,area=0*wid也就是area=0;这个结果显然是错误的,所以A操作是不能和C操作进行重排序的(这就是注2中说的前一个操作的执行结果必须对后羿操作可见,如果不满足这个要求就不允许这两个操作进行重排序)

    2. volatile 的效果

    1. 保证变量的可见性
    2. 保证 volatile 变量和普通变量重排序(jdk 5 之后才有的)

    2.1 内存可见性

    这里编写一个例子

    
    public class VolatileTest(
    
    	static class Variable{
            static int a = 1;
            static boolean flag = false;
        }
    
        public static void main(String []args){
    
            new Thread(()->{
                Variable.a ++;   // step 1
                Variable.flag = true;   // step 2
    
            }).start();
            new Thread(()->{
                if (Variable.flag){
                    System.out.println(Variable.a);  // step 3
                }
            }).start();
    
        }
    
    )
    
    

    上面的代码看似没有任何问题,但是如果不适用 volatile 来修饰 flag 的话,flag 和 a 就有可能发生指令重排序,如果发生了重排序,flag = true; 先执行,如果这时下面的那个线程得到了时间片,就会打印出 a = 0;

    所以正确的代码应该是:

    
    public class VolatileTest(
    
    	static class Variable{
            static int a = 0;
            static volatile boolean flag = false;
        }
    
        public static void main(String []args){
    
            new Thread(()->{
                Variable.a ++;   // step 1
                Variable.flag = true;   // step 2
    
            }).start();
            new Thread(()->{
                if (Variable.flag){
                    System.out.println(Variable.a);  // step 3
                }
            }).start();
    
        }
    
    )
    
    

    在使用了 volatile 保证了 Variable.a ++ 一定是先执行时,还有一个问题需要进行分析,那就是内存可见性。Variable.flag = true; 并非是一个原子操作,在底层的实际步骤是:

    1. 获取 flag 的值放入寄存器
    2. 将寄存器中的值修改为false
    3. 将寄存处器中的值存回内存,即赋值给 Variable.flag

    保证可见性的实际步骤如下图:
    在这里插入图片描述

    2.2 内存屏障

    在JSR-133之前的旧的Java内存模型中,是允许volatile变量与普通变量重排序的。
    那上⾯的案例中,可能就会被重排序成下列时序来执⾏:

    1. 线程A写volatile变量,step 2,设置flag为true;
    2. 线程B读同⼀个volatile,step 3,读取到flag为true;
    3. 线程B读普通变量,step 4,读取到 a = 0;
    4. 线程A修改普通变量,step 1,设置 a = 1;

    可⻅,如果volatile变量与普通变量发⽣了重排序,虽然volatile变量能保证内存可⻅
    性,也可能导致普通变量读取错误。
    所以在旧的内存模型中,volatile的写-读就不能与锁的释放-获取具有相同的内存语
    义了。为了提供⼀种⽐锁更轻量级的线程间的通信机制,JSR-133专家组决定增强
    volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序。
    编译器还好说,JVM是怎么还能限制处理器的重排序的呢?它是通过内存屏障来实
    现的。

    什么是内存屏障?硬件层⾯,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作⽤:

    1. 阻⽌屏障两侧的指令重排序;
    2. 强制把写缓冲区/⾼速缓存中的脏数据等写回主内存,或者让缓存中相应的数据
      失效。

    编译器在⽣成字节码时,会在指令序列中插⼊内存屏障来禁⽌特定类型的处理器重排序。编译器选择了⼀个⽐较保守的JMM内存屏障插⼊策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:

    • 在每个volatile写操作前插⼊⼀个StoreStore屏障;
    • 在每个volatile写操作后插⼊⼀个StoreLoad屏障;
    • 在每个volatile读操作后插⼊⼀个LoadLoad屏障;
    • 在每个volatile读操作后再插⼊⼀个LoadStore屏障。

    在这里插入图片描述

    对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
    对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
    

    java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

    • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

    由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。

    举个例⼦,我们在案例中step 1,是普通变量的写,step 2是volatile变量的写,那符合第2个规则,这两个steps不能重排序。⽽step 3是volatile变量读,step 4是普通变量读,符合第1个规则,同样不能重排序。

    但如果是下列情况:第⼀个操作是普通变量读,第⼆个操作是volatile变读,那是可以重排序的:

    // 声明变量
    int a = 0; // 声明普通变量
    volatile boolean flag = false; // 声明volatile变量
    // 以下两个变量的读操作是可以重排序的
    int i = a; // 普通变量读
    boolean j = flag; // volatile变量读
    

    3. volatile 的用途

    从volatile的内存语义上来看,volatile可以保证内存可⻅性且禁⽌重排序。

    在保证内存可⻅性这⼀点上,volatile有着与锁相同的内存语义,所以可以作为⼀个“轻量级”的锁来使⽤。但由于volatile仅仅保证对单个volatile变量的读/写具有原⼦性,⽽锁可以保证整个临界区代码的执⾏具有原⼦性。所以在功能上,锁⽐volatile更强⼤;在性能上,volatile更有优势

    3.1 单例模式双重检查锁

    public class SinglaMode{
    	
    	private final volatile SinglaMode instance;
    
    	private SinglaMode(){};
    
    	public SinglaMode getInstance(){
    	
    		if(mode == null){
    			synchronized(this){
    				if(mode == null){
    					instance = new SinglaMode();
    				}
    			}
    		}
    		return instance;
    	}
    
    }
    
    

    如果这⾥的变量声明不使⽤volatile关键字,是可能会发⽣错误的。它可能会被重排序:

    instance = new SingleMode(); // 第10⾏
    // 可以分解为以下三个步骤
    1 memory=allocate();// 分配内存 相当于c的malloc
    2 ctorInstanc(memory) //初始化对象
    3 s=memory //设置s指向刚分配的地址
    // 上述三个步骤可能会被重排序为 1-3-2,也就是:
    1 memory=allocate();// 分配内存 相当于c的malloc
    3 s=memory //设置s指向刚分配的地址
    2 ctorInstanc(memory) //初始化对象
    
    

    ⽽⼀旦假设发⽣了这样的重排序,⽐如线程A在第10⾏执⾏了步骤1和步骤3,但是步骤2还没有执⾏完。这个时候线程A执⾏到了第7⾏,它会判定instance不为空,然后直接返回了⼀个未初始化完成的instance!

    所以JSR-133对volatile做了增强后,volatile的禁⽌重排序功能还是⾮常有⽤的。

    展开全文
  • volatile我们已经再熟悉不过了,他保证了属性的可见性并且禁止指令重排 对于可见性:也就是一个线程改了值,另外一个线程可以立马看到结果,也就是但操作是原子的 对于指令重排,有两种:一种是编译重排,一种是...

    编译器有指令优化技术,处理器有超标量技术,都将会导致没有依赖的指令乱序执行来达到优化性能的目的,所有有的禁止重排序

    而多个处理器缓存的数据可能不一致,就有了MISE协议来保证缓存的一致性,但是如果CPU0写了一个不在缓存的值,那么会发送一个读并失效的消息,CPU 1收到这个消息后会将返回新值并使本地缓存失效,这么一个同步过程是比较耗时的,所以硬件提供了store buffer和无效队列,来达到一个异步处理缓存不一致的问题,但是也会导致乱序执行的代码出现数据不同步问题,所以引入了内存屏障,读屏障在读操作前会先处理完所有的无效队列,写屏障前会冲刷所有的storebuffer

    Linux内核的有些函数隐含内存屏障。(1)获取和释放函数。(2)中断禁止函数

    1.获取和释放函数获取(acquire)函数包括如下。

    (1)获取锁的函数。锁包括自旋锁、读写自旋锁、互斥锁、信号量和读写信号量。

    (2)smp_load_acquire(p):加载获取。

    (3)smp_cond_load_acquire(ptr, cond_expr):带条件的加载获取。

    获取操作隐含如下。

    (1)获取操作后面的内存访问操作只能在获取操作完成之后被观察到。

    (2)获取操作前面的内存访问操作可能在获取操作完成之后被观察到。

    释放(release)函数包括如下。

    (1)释放锁的函数。

    (2)smp_store_release(p, v):存储释放。

    释放操作隐含如下。

    (1)释放操作前面的内存访问操作必须在释放操作完成之前被观察到。

    (2)释放操作后面的内存访问操作可能在释放操作完成之前被观察到。获取操作和释放操作都是单向屏障。

    2.中断禁止函数禁止中断和开启中断的函数只充当编译器优化屏障。

    volatile我们已经再熟悉不过了,他保证了属性的可见性并且禁止指令重排

    对于可见性:也就是一个线程改了值,另外一个线程可以立马看到结果,也就是但操作是原子的

    对于指令重排,有两种:一种是编译重排,一种是CPU重排(运行期重排),编译重排这里的编译不是变成橙字节码,而是JVM解释成字节码的时候的重排,

    那么这两种重排都会有一些保证:编译器重排有一些happenbefore规则,在不违反这些原则的基础上进行重排,而有一些指令他是在编译期间就能知道顺序的,比如说new Object()操作包含以下几步:

    1.在堆区(TLAB)分配内存,生成一个不完全对象(空壳子)

    2.将不完全对象的引用压入栈顶 dup

    3.赋值栈顶元素,将复制的数值压入栈顶 执行invokespecial (构造函数)

    4.赋值this指针

    5.堆区中的对象就是一个完整的对象

    6.putstatic 将对象赋值给instance 或者是其他的引用

    但是有些逻辑我们是在编译器不能确定顺序的,就需要JVM提供一种机制来为程序员提供控制,也就是内存屏障机制,而JVM是借助CPU的内存屏障来实现的。

    而对于happen before规则,因为其中的一些操作是没有依赖性的,比如初始化和赋值给引用,这样,编译器可能会对他进行指令重拍,CPU也可能进行重排,而编译器可能知道这个是有顺序性的,可能不会对其进行重排,但是CPU可能会对这些指令进行重排,这样就可能导致instance拿到的是一个没有初始化的对象,导致错误

    那么我们就需要一种方式来禁止这种重排:

    编译重排(编译器):有编译屏障来禁止重排,当然JVM也可以选择不通的编译级别,在不通的级别,编译可能会进行不同程度的重排,这些重排都是遵行happen before规则的,这也是JVM的规范

    CPU重排(运行期):因为CPU的指令可能分为多个步骤,那么他可以他一些步骤来并行的执行,所以可能会进行一些重排序,但是它也不会随便重排,也有一些保证,也就是as-if-serial语义,也就是保证重排后的指令执行结果跟单线程执行结果是一样的。

    对于指令重排,CPU提供了两种解决方式:

    fence簇(内存屏障):sfence ,lfence ,mfence(串行化读写,队列化读)

    lock指令(这也是JVM的volatile实现方式):缓存锁,或总线锁

    对于CPU的写有两种方式:

    一种是同步写,CPU将值写入store buff,然后刷入缓存,然后通过ESMI协议来通知其他的CPU或者是刷入主存

    还有一种是异步写:CPU将值写入store buffer后就去干其他事了,等CPU空闲后再将值写入缓存,然后通过一致性协议来通知其他核心

    显然第二种方式是比较快的,而且现在的大部分CPU都是第二种方式写。

    而第一种方式虽然能够让CPU具有很高的性能和效率,但是对于一些可见性要求比较高的业务是不友好的,一个CPU写入值之后并没有刷入缓存并通过ESMI协议通知其他核心,就会有同步问题,虽然在大部分情况下本来就不保证同步性的,但是还是需要一种机制来保证这种同步性,那么内存屏障就是为这设计的

    lock锁定了缓存或者是总线,那么在获取锁和释放锁的时候就会保证storebuff的值刷入缓存,这保证了有序性,也间接的保证了可见性(还有一种说法是因为有lock,让CPU空闲下来了,让storebuff刷入了缓存,保证了可见性)

    总结:

    as-if-serial:是CPU对指令重排的保证

    happen-before:有些指令是在编译的时候就能确定顺序的,比如一些具有依赖性的指令,这些指令编译器不会对其重拍

    内存屏障:为了解决不同CPU核心的执行顺序乱序导致数据不一致的问题而产生的        

            

    CPU 对内存进行操作的时候,顺序和程序代码指令顺序不一致。在写操作执行之前就先执行了读操作。另一个原因是在同一个 CPU 中同一个数据存在不一致的情况 , 在 store buffer 中是最新的数据, 在 cache line 中是旧的数据。为了解决在同一个 CPU 的 store buffer 和 cache 之间数据不一致的问题,引入了 写屏障

    许多CPU architecture提供了弱一点的memory barrier指令只mark其中之一。如果只mark invalidate queue,那么这种memory barrier被称为read memory barrier。相应的,write memory barrier只mark store buffer。一个全功能的memory barrier会同时mark store buffer和invalidate queue。

    我们一起来看看读写内存屏障的执行效果:对于read memory barrier指令,它只是约束执行CPU上的load操作的顺序,具体的效果就是CPU一定是完成read memory barrier之前的load操作之后,才开始执行read memory barrier之后的load操作。read memory barrier指令象一道栅栏,严格区分了之前和之后的load操作。同样的,write memory barrier指令,它只是约束执行CPU上的store操作的顺序,具体的效果就是CPU一定是完成write memory barrier之前的store操作之后,才开始执行write memory barrier之后的store操作。全功能的memory barrier会同时约束load和store操作,当然只是对执行memory barrier的CPU有效。

          

    具体可以参考:内存屏障(Memory Barrier)究竟是个什么鬼? - 知乎

    lock指令:缓存锁或者总线锁

    Linux中的同步原语:
    pre-cpu variable: 一个CPU不应该访问与其他CPU对应的数组元素,另外,他可以随意读或修改他自己的元素而不用担心出现竞争条件,因为他是唯一有资格这么做的CPU,但是,这也意味着pre-cpu vairable基本上只能在特殊情况下使用,也就是当他确定在系统的CPU上的数据在逻辑上是独立的时候。
    原子操作:80x86的一些原子指令:
        1.进行零次或一次对齐内存访问的汇编指令是原子的(当数据项的地址是以字节为单位的整数倍时,数据项在内存中被对齐。例如一个对齐的短整数的地址必须是2的整数倍,而对齐的整数的地址必须是4的整数倍。一般来说非对齐的内存访问不是原子的)
        2.如果在读操作之后,写操作之前没有其他处理器占用内存总线,那么从内存中读取数据,更新数据并把更新数据写回内存的这些“读-修改-写”汇编语言指令(例如inc 或dec)是原子的
        3.操作码前缀是lock字节(0xf0)的“读-修改-写”汇编语言指令即使在多处理器系统中也是原子的。当控制单元检测到这个前缀时,就锁定内存总线,直到这条指令指令完成。因此,当加锁的指令执行时,其他处理器不能访问这个内存单元。
        4.LInux提供了专门的atomic_t类型(一个原子访问计数器)和一些专门的函数和宏,这些函数和宏作用于atomic_t类型的变量,并当作单独的,原子的汇编指令来使用。在多处理系统中,每条这样的指令都有一个lock字节的前缀
            atomic_read,atomic_set,atomic_add,atomic_sub,atomic_inc 等等
    优化和内存屏障
        CPU通常并行地执行若干条指令,且可能重新安排内存访问。这种重新排序可以极大的加速程序的执行
        然而,当处理同步时,必须避免指令重排序。如果放在同步原语之后的一条指令在同步原语本身之前执行,事情就会变得失控。事实上,所有的同步原语起优化和内存屏障的作用
        优化屏障原语保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言指令,这些汇编语言指令在C中都有对应的语句。优化屏障也可以叫做编译屏障。
        内存屏障确保,在原语之后的操作开始执行之前,原语之前的操作已经完成(该写的值都已经写入内存)。
    自旋锁:Linux中,每个自旋锁都用spinlock_t结构表示其中两个字段slock表示自旋锁状态,break_lock表示正在忙等自旋锁
        自旋锁的宏:spin_lock_init(),spin_lock(),spin_unlock(),spin_unlock_wait(),spin_is_locked(),spin_trylock();
        自旋锁会参与Cpu抢占的,自旋锁主要是通过xchg 汇编指令原子性的交换寄存器内存单元的内容。然后通过结果进行判断是否自旋
    顺序锁
    读-拷贝-更新(RCU)
    信号量:和自旋锁不一样的是,自旋锁会自旋,但是信号量会挂起进程

    展开全文
  • 在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器、运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要。否则,你很难搞清楚哪些操作是...

     在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器、运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要。否则,你很难搞清楚哪些操作是在并发线程中绝对安全的?哪些是相对安全的?哪些并发同步手段性能最低?valotile的二层语义分别是什么,等?

    一、什么是重排序

    请先看这样一段代码

    package com.cby.jvmOrder;
    
    /**
     * Created by cby on 2018/10/16.
     */
    public class PossibleReordering {
        static int x = 0, y = 0;
        static int a = 0, b = 0;
    
            public static void main(String[] args) throws InterruptedException {
                 for (int i=0;i<100;i++){
                     testThread();
                 }
            }
            public static void testThread() throws InterruptedException{
                Thread one = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        a = 1;
                        x = b;
                    }
                });
    
                Thread other = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        b = 1;
                        y = a;
                    }
                });
                one.start();
                other.start();
                one.join();
                other.join();
                System.out.println("(" + x + "," + y + ")");
            }
    }

    结果:(0,1) 或者 (1,1) 或者 (1,0)

          很容易想到这段代码的运行结果可能为(1,0)、(0,1)或(1,1),因为线程one可以在线程two开始之前就执行完了,也有可能反之,甚至有可能二者的指令是同时或交替执行的。

          然而,这段代码的执行结果也可能是(0,0). 因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。得到(0,0)结果的语句执行过程,如下图所示。值得注意的是,a=1和x=b这两个语句的赋值操作的顺序被颠倒了,或者说,发生了指令“重排序”(reordering)。(事实上,输出了这一结果,并不代表一定发生了指令重排序,内存可见性问题也会导致这样的输出,详见后文)

       

         对重排序现象不太了解的开发者可能会对这种现象感到吃惊,但是,笔者开发环境下做的一个小实验证实了这一结果。

         实验代码是构造一个循环,反复执行上面的实例代码,直到出现a=0且b=0的输出为止。实验结果说明,循环执行到第13830次时输出了(0,0)。

         大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。
         除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。

    二、as-if-serial语义

         As-if-serial语义的意思是,所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。
         比如,为了保证这一语义,重排序不会发生在有数据依赖的操作之中。

    int a = 1;
    int b = 2;
    int c = a + b;

          将上面的代码编译成Java字节码或生成机器指令,可视为展开成了以下几步动作(实际可能会省略或添加某些步骤)。

    1. 对a赋值1
    2. 对b赋值2
    3. 取a的值
    4. 取b的值
    5. 将取到两个值相加后存入c

          在上面5个动作中,动作1可能会和动作2、4重排序,动作2可能会和动作1、3重排序,动作3可能会和动作2、4重排序,动作4可能会和1、3重排序。但动作1和动作3、5不能重排序。动作2和动作4、5不能重排序。因为它们之间存在数据依赖关系,一旦重排,as-if-serial语义便无法保证。

          为保证as-if-serial语义,Java异常处理机制也会为重排序做一些特殊处理。例如在下面的代码中,y = 0 / 0可能会被重排序在x = 2之前执行,为了保证最终不致于输出x = 1的错误结果,JIT在重排序时会在catch语句中插入错误代偿代码,将x赋值为2,将程序恢复到发生异常时应有的状态。这种做法的确将异常捕捉的逻辑变得复杂了,但是JIT的优化的原则是,尽力优化正常运行下的代码逻辑,哪怕以catch块逻辑变得复杂为代价,毕竟,进入catch块内是一种“异常”情况的表现。

    package com.cby.jvmOrder;
    
    /**
     * Created by cby on 2018/10/16.
     */
    public class Reordering {
        public static void main(String[] args) {
             int x, y;
             x = 1;
             try {
                     x = 2;
                     y = 0 / 0;
             } catch (Exception e) {
    
             } finally {
                 System.out.println("x = " + x);
             }
         }
    }
     

    三、内存访问重排序与内存可见性

          计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。其模型如下图所示。

         在这种模型下会存在一个现象,即缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。这导致在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值可能是不一致的。
         有的观点会将这种现象也视为重排序的一种,命名为“内存系统重排序”。因为这种内存可见性问题造成的结果就好像是内存访问指令发生了重排序一样。
         这种内存可见性问题也会导致章节一中示例代码即便在没有发生指令重排序的情况下的执行结果也还是(0, 0)。

    四、内存访问重排序与Java内存模型

          Java的目标是成为一门平台无关性的语言,即Write once, run anywhere. 但是不同硬件环境下指令重排序的规则不尽相同。例如,x86下运行正常的Java程序在IA64下就可能得到非预期的运行结果。为此,JSR-1337制定了Java内存模型(Java Memory Model, JMM),旨在提供一个统一的可参考的规范,屏蔽平台差异性。从Java 5开始,Java内存模型成为Java语言规范的一部分。
         根据Java内存模型中的规定,可以总结出以下几条happens-before规则。Happens-before的前后两个操作不会被重排序且后者对前者的内存可见。

    • 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
    • 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
    • volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
    • 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
    • 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
    • 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
    • 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
    • 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C

          Happens-before关系只是对Java内存模型的一种近似性的描述,它并不够严谨,但便于日常程序开发参考使用,关于更严谨的Java内存模型的定义和描述,请阅读JSR-133原文或Java语言规范章节17.4。

          除此之外,Java内存模型对volatile和final的语义做了扩展。对volatile语义的扩展保证了volatile变量在一些情况下不会重排序,volatile的64位变量double和long的读取和赋值操作都是原子的。对final语义的扩展保证一个对象的构建方法结束前,所有final成员变量都必须完成初始化(的前提是没有this引用溢出)。

          Java内存模型关于重排序的规定,总结后如下表所示。

          表中“第二项操作”的含义是指,第一项操作之后的所有指定操作。如,普通读不能与其之后的所有volatile写重排序。另外,JMM也规定了上述volatile和同步块的规则尽适用于存在多线程访问的情景。例如,若编译器(这里的编译器也包括JIT,下同)证明了一个volatile变量只能被单线程访问,那么就可能会把它做为普通变量来处理。
          留白的单元格代表允许在不违反Java基本语义的情况下重排序。例如,编译器不会对对同一内存地址的读和写操作重排序,但是允许对不同地址的读和写操作重排序。

          除此之外,为了保证final的新增语义。JSR-133对于final变量的重排序也做了限制。

      • 构建方法内部的final成员变量的存储,并且,假如final成员变量本身是一个引用的话,这个final成员变量可以引用到的一切存储操作,都不能与构建方法外的将当期构建对象赋值于多线程共享变量的存储操作重排序。例如对于如下语句
        x.finalField = v; ... ;构建方法边界sharedRef = x;
        v.afield = 1; x.finalField = v; ... ; 构建方法边界sharedRef = x;
        这两条语句中,构建方法边界前后的指令都不能重排序。
      • 初始读取共享对象与初始读取该共享对象的final成员变量之间不能重排序。例如对于如下语句
        x = sharedRef; ... ; i = x.finalField;
        前后两句语句之间不会发生重排序。由于这两句语句有数据依赖关系,编译器本身就不会对它们重排序,但确实有一些处理器会对这种情况重排序,因此特别制定了这一规则。

    五、内存屏障

          内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
          内存屏障可以被分为以下几种类型
    LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。        在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

           有的处理器的重排序规则较严,无需内存屏障也能很好的工作,Java编译器会在这种情况下不放置内存屏障。
           为了实现上一章中讨论的JSR-133的规定,Java编译器会这样使用内存屏障。

         为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。
         x.finalField = v; StoreStore; sharedRef = x;

    六、Intel 64/IA-32架构下的内存访问重排序

         Intel 64和IA-32是我们较常用的硬件环境,相对于其它处理器而言,它们拥有一种较严格的重排序规则。Pentium 4以后的Intel 64或IA-32处理的重排序规则如下。9

    在单CPU系统中

    • 读操作不与其它读操作重排序。
    • 写操作不与其之前的写操作重排序。
    • 写内存操作不与其它写操作重排序,但有以下几种例外
    • CLFLUSH的写操作
    • 带有non-temporal move指令(MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, and MOVNTPD)的streaming写入。
    • 字符串操作
    • 读操作可能会与其之前的写不同位置的写操作重排序,但不与其之前的写相同位置的写操作重排序。
    • 读和写操作不与I/O指令,带锁的指令或序列化指令重排序。
    • 读操作不能重排序到LFENCE和MFENCE之前。
    • 写操作不能重排序到LFENCE、SFENCE和MFENCE之前。
    • LFENCE不能重排序到读操作之前。
    • SFENCE不能重排序到写之前。
    • MFENCE不能重排序到读或写操作之前。

    在多处理器系统中

    • 各自处理器内部遵循单处理器的重排序规则。
    • 单处理器的写操作对所有处理器可见是同时的。
    • 各自处理器的写操作不会重排序。
    • 内存重排序遵守因果性(causality)(内存重排序遵守传递可见性)。
    • 任何写操作对于执行这些写操作的处理器之外的处理器来看都是一致的。
    • 带锁指令是顺序执行的。

         值得注意的是,对于Java编译器而言,Intel 64/IA-32架构下处理器不需要LoadLoad、LoadStore、StoreStore屏障,因为不会发生需要这三种屏障的重排序。

    七、一例Intel 64/IA-32架构下的代码性能优化

          现在有这样一个场景,一个容器可以放一个东西,容器支持create方法来创建一个新的东西并放到容器里,支持get方法取到这个容器里的东西。我们可以较容易地写出下面的代码。

     

    package com.cby.jvmOrder;
    
    /**
     * Created by cby on 2018/10/16.
     */
    public class Container {
        public static class SomeThing {
              private int status;
                  public SomeThing() {
                         status = 1;
                     }
    
                  public int getStatus() {
                         return status;
                    }
         }
         private SomeThing object;
         public void create() {
             object = new SomeThing();
         }
        public SomeThing get() {
                while (object == null) {
                    //不加这句话可能会在此出现无限循环
                   Thread.yield();
               }
            return object;
        }
    }
    

          在单线程场景下,这段代码执行起来是没有问题的。但是在多线程并发场景下,由不同的线程create和get东西,这段代码是有问题的。问题的原因与普通的双重检查锁定单例模式(Double Checked Locking, DCL)10类似,即SomeThing的构建与将指向构建中的SomeThing引用赋值到object变量这两者可能会发生重排序。导致get中返回一个正被构建中的不完整的SomeThing对象实例。为了解决这一问题,通常的办法是使用volatile修饰object字段。这种方法避免了重排序,保证了内存可见性,摒弃比使用同步块导致的性能损失更小。但是,假如使用场景对object的内存可见性并不敏感的话(不要求一个线程写入了object,object的新值立即对下一个读取的线程可见),在Intel 64/IA-32环境下,有更好的解决方案。

         根据上一章的内容,我们知道Intel 64/IA-32下写操作之间不会发生重排序,即在处理器中,构建SomeThing对象与赋值到object这两个操作之间的顺序性是可以保证的。这样看起来,仅仅使用volatile来避免重排序是多此一举的。但是,Java编译器却可能生成重排序后的指令。但令人高兴的是,Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong这三个方法,JDK会在执行这三个方法时插入StoreStore内存屏障,避免发生写操作重排序。而在Intel 64/IA-32架构下,StoreStore屏障并不需要,Java编译器会将StoreStore屏障去除。比起写入volatile变量之后执行StoreLoad屏障的巨大开销,采用这种方法除了避免重排序而带来的性能损失以外,不会带来其它的性能开销。
         我们将做一个小实验来比较二者的性能差异。一种是使用volatile修饰object成员变量。

    package com.cby.jvmOrder;
    
    /**
     * Created by cby on 2018/10/16.
     */
    public class Container {
        public static class SomeThing {
              private int status;
                  public SomeThing() {
                         status = 1;
                     }
    
                  public int getStatus() {
                         return status;
                    }
         }
         private SomeThing object;
         public void create() {
             object = new SomeThing();
         }
        public SomeThing get() {
                while (object == null) {
                    //不加这句话可能会在此出现无限循环
                   Thread.yield();
               }
            return object;
        }
    }
    
      

     

         一种是利用Unsafe. putOrderedObject在避免在适当的位置发生重排序。

     

    package com.cby.jvmOrder;
    
    import sun.misc.Unsafe;
    
    import java.lang.reflect.Field;
    
    /**
     * Created by cby on 2018/10/16.
     */
    public class ContainerO {
        public static class SomeThing {
              private int status;
    
                  public SomeThing() {
                         status = 1;
                     }
    
                  public int getStatus() {
                         return status;
                     }
         }
         private SomeThing object;
    
         private Object value;
         private static final Unsafe unsafe = getUnsafe();
         private static final long valueOffset;
         static {
                 try {
                     valueOffset = unsafe.objectFieldOffset(Container.class.getDeclaredField("value"));
                     } catch (Exception ex) { throw new Error(ex); }
                 }
                 public void create() {
                     SomeThing temp = new SomeThing();
                     //将value赋null值只是一项无用操作,实际利用的是这条语句的内存屏障
                     unsafe.putOrderedObject(this, valueOffset, null);              
                     //unsafe.putObjectVolatile(this, valueOffset, null);
                     object = temp;
                 }
    
                 public SomeThing get() {
                    while (object == null) {
                             Thread.yield();
                         }
                    return object;
                 }
    
    
                 public static Unsafe getUnsafe() {
                     try {
                             Field f = Unsafe.class.getDeclaredField("theUnsafe");
                             f.setAccessible(true);
                             return (Unsafe)f.get(null);
                         } catch (Exception e) {
                         }
                     return null;
                 }
    }
    

     

         由于直接调用Unsafe.getUnsafe()需要配置JRE获取较高权限,我们利用反射获取Unsafe中的theUnsafe来取得Unsafe的可用实例。
         unsafe.putOrderedObject(this, valueOffset, null)
         这句仅仅是为了借用这句话功能的防止写重排序,除此之外无其它作用。

         利用下面的代码分别测试两种方案的实际运行时间。在运行时开启-server和 -XX:CompileThreshold=1以模拟生产环境下长时间运行后的JIT优化效果。

    package com.cby.jvmOrder;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * Created by cby on 2018/10/16.
     */
    public class Main {
        public static void main(String[] args) throws InterruptedException {
                 final int THREADS_COUNT = 20;
                 final int LOOP_COUNT = 100000;
                 long sum = 0;
                 long min = Integer.MAX_VALUE;
                 long max = 0;
                 for(int n = 0;n <= 100;n++) {
                         final Container basket = new Container();
                         List<Thread> putThreads = new ArrayList<Thread>();
                         List<Thread> takeThreads = new ArrayList<Thread>();
                         for (int i = 0; i < THREADS_COUNT; i++) {
                             putThreads.add(new Thread() {
                                 @Override
                                 public void run() {
                                     for (int j = 0; j < LOOP_COUNT; j++) {
                                         basket.create();
                                     }
                                 }
                             });
                             takeThreads.add(new Thread() {
                                 @Override
                                 public void run() {
                                     for (int j = 0; j < LOOP_COUNT; j++) {
                                         basket.get().getStatus();
                                     }
                                 }
                             });
                         }
                         long start = System.nanoTime();
                         for (int i = 0; i < THREADS_COUNT; i++) {
                                 takeThreads.get(i).start();
                                 putThreads.get(i).start();
                         }
                         for (int i = 0; i < THREADS_COUNT; i++) {
                                 takeThreads.get(i).join();
                                putThreads.get(i).join();
                         }
                         long end = System.nanoTime();
                         long period = end - start;
                         if(n == 0) {
                            //由于JIT的编译,第一次执行需要更多时间,将此时间不计入统计
                             continue;
                         }
                         sum += (period);
                         System.out.println(period);
                         if(period < min) {
                                 min = period;
                          }
                         if(period > max) {
                                max = period;
                         }
                    }
                    System.out.println("Average : " + sum / 100);
                    System.out.println("Max : " + max);
                    System.out.println("Min : " + min);
             }
    }
    

    在笔者的计算机上运行测试,采用volatile方案的运行结果如下
    Average : 62535770
    Max : 82515000
    Min : 45161000

    采用unsafe.putOrderedObject方案的运行结果如下
    Average : 50746230
    Max : 68999000
    Min : 38038000

          从结果看出,unsafe.putOrderedObject方案比volatile方案平均耗时减少18.9%,最大耗时减少16.4%,最小耗时减少15.8%.另外,即使在其它会发生写写重排序的处理器中,由于StoreStore屏障的性能损耗小于StoreLoad屏障,采用这一方法也是一种可行的方案。但值得再次注意的是,这一方案不是对volatile语义的等价替换,而是在特定场景下做的特殊优化,它仅避免了写写重排序,但不保证内存可见性。

     

    复现重排序现象实验代码

    package com.cby.jvmOrder;
    
    /**
     * Created by cby on 2018/10/16.
     */
    public class Test {
        private static int x = 0, y = 0;
        private static int a = 0, b =0;
    
        public static void main(String[] args) throws InterruptedException {
            int i = 0;
            for(;;) {
                i++;
                x = 0; y = 0;
                a = 0; b = 0;
                Thread one = new Thread(new Runnable() {
                    @Override
                    public void run() {
                         //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                         shortWait(100000);
                         a = 1;
                         x = b;
                     }
                 });
    
                Thread other = new Thread(new Runnable() {
                     @Override
                     public void run() {
                        b = 1;
                        y = a;
                        }
                });
               one.start();other.start();
                 one.join();other.join();
                String result = "第" + i + "次 (" + x + "," + y + ")";
                if(x == 0 && y == 0) {
                       System.err.println(result);
                        break;
                     } else {
                         System.out.println(result);
                     }
             }
        }
    
    
         public static void shortWait(long interval){
             long start = System.nanoTime();
             long end;
             do{
                    end = System.nanoTime();
              }while(start + interval >= end);
         }
    }

    八 参考文献

    https://www.cnblogs.com/langtianya/p/3898760.html

    https://stackoverflow.com/questions/9341083/how-to-use-xxunlockdiagnosticvmoptions-xxcompilecommand-print-option-with-j/9415368#9415368

    https://blog.csdn.net/zyc88888/article/details/64905716

    展开全文
  • Volatile内存屏障&指令重排

    千次阅读 2020-09-18 22:19:13
    指令重排前言内存屏障CPU缓存架构之伪共享指令重排as-if-serialhappens-beforeJIT指令重排序指令重排序的手段volatile重排序规则 前言 在上一篇笔记中提到了volatile关键字的作用,我们知道并发编程的三大理论是可见...
  • 线程、工作内存与主存之间的关系 内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外) lock ...
  • 一、指令重排 什么是指令重排?      java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果 它顺序化情况的结果相等,那么指令的执行顺序可以代码顺序不一致,此过程叫指令的重排序。 指令重排...
  • 关于第一步第二步JMM内存屏障插入策略代码案例分析参考资料 前言 前面博客中,重点说明了volatile的相关特性。 比如:保证共享变量在其他线程中的及时可见性,不能保证原子性。以及包括计算机指令重排的优点和缺点,...
  • 内存模型:Go如何保证并发读写的顺序? Go 官方文档里专门介绍了 Go 的内存模型,你不要误解这里的内存模型的...由于 CPU 指令重排和多级 Cache 的存在,保证多核访问同一个变量这件事儿变得非常复杂。毕竟,不同 CPU
  • 内存屏障&指令重排序

    2020-08-19 23:36:06
    这里写目录标题什么是内存屏障内存屏障与处理器重排序内存屏障与volatile 什么是内存屏障 内存屏障它是一条CPU指令: 确保一些特定操作执行的顺序; 影响一些数据的可见性(可能是某些指令执行后的结果)。 内存...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 8,303
精华内容 3,321
关键字:

内存屏障与指令重排