搞透并发编程--有序性

代码中可能存在的有序性问题

as-if-serial:

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序

happens-before:

避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性,如果从代码不存在以下规则的约束,那么编译器,处理器可以对他们进行重排序;
1.程序次序原则
一个线程中,按照程序的顺序,前面的操作happens-before后续的任何操作。
2.volatile规则
一个volatile的写操作,happends-before后面的读操作
3.传递性规则
如果A happens-before B,B happens-before C,那么A happens-before C。
4.管程中的锁规则
对一个锁的解锁操作,happens-before后续对这个锁的加锁操作。
5.线程start()规则
线程A启动线程B,线程B中可以看到线程A启动B之前的操作。也就是start() happens before 线程B中的操作。
6.线程终结原则
join()操作会使当前线程等待join()线程执行结束, 线程A等待线程B完成,当线程B执行完毕后,线程A可以看到线程B的所有操作。也就是说,线程B中的任意操作,happens-before join()的返回。
7.线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
8.对象终结原则
一个对象的实例化方法先行发生于他的finalize()方法;

上一串代码来演示存在有序性的问题;

public class VolatileDemo1 {
    private static int a=0,b=0,x=0,y=0;
    private static int index;

    public static void main(String[] args) throws InterruptedException {
        for (;;){
            a=0;b=0;x=0;y=0;
            Thread t1=new Thread(new Runnable() {
                @Override
                public void run() {
                    a=1;
                    x=b;
                }
            });

            Thread t2=new Thread(new Runnable() {
                @Override
                public void run() {
                    b=1;
                    y=a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            index++;

            System.err.println("第"+index+"次,x="+x+"y="+y);
            if(x==0&&y==0){
                break;
            }
        }
    }
}

在这里插入图片描述
下面以时序图来解释具体运行过程;
不考虑指令重排的情况下可能出现3中情况:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

发生指令重排以后,可能会返回0,0
可以从happens-before原则中推导一下,看看是否满足不能重排序的规则;
1,程序次序原则;
只单独看t1,t2 run() 方法中的代码之间是没有任何联系的,满足as-if-serial的语义,可以进行重排序;
2.没有volatile关键字,没有线程之间的相互启动,中断,终结,没有对象的终结; 那么这里的代码可以进行重排序;

volatile的第二个语义

volatile 一个语义是保证可见性,在上一篇文章已经说过了, 那么第二个语义就是禁止重排序了; 上述代码中将x,y 分别添加volatile后,将不会再出现指令重排序的情况;

volatile如何禁止指令重排序的呢?

上一篇曾说过,添加了volatile的变量转成汇编代码后 会生成一个lock前缀,来保证可见性; 这个lock 还有个功能是它能完成类似内存屏障的功能

Intel硬件提供了一系列的内存屏障,主要有:

  1. lfence,是一种Load Barrier 读屏障
  2. sfence, 是一种Store Barrier 写屏障
  3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
  4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执 行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于 编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉 编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插 入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用 是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由 JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:

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

在这里插入图片描述
可以总结为三条:
1.当第二个操作是volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile 写之前的操作不会被编译器重排序到volatile 写之后
2.当第一个操作是volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile 读之后的操作不会被编译器重排序到volatile 读之前。
3.当第一个操作是volatile 写,第二个操作是volatile 读时,不能重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障 来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的 总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略.

1.在每个volatile写操作的前面插入一个StoreStore屏障。
2.在每个volatile写操作的后面插入一个StoreLoad屏障。
3.在每个volatile读操作的后面插入一个LoadLoad屏障。
4.在每个volatile读操作的后面插入一个LoadStore屏障。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值