Java有序性问题深入分析 & 内存屏障多层面深入理解

一、有序性问题深入分析

思考:下面的Java程序中x和y的最终结果是什么?

public class ReOrderTest {

    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;
        while (true) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            /**
             *  x,y:
             */
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    shortWait(20000);
                    a = 1;
                    x = b;

                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });

            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            System.out.println("第" + i + "次(" + x + "," + y + ")");

            if (x == 0 && y == 0){
                break;
            }

        }

    }

    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }

二、指令重排序

ava语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

指令重排序的意义 JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

编译器与CPU处理器中都能执行指令重排优化操作。

在这里插入图片描述

2.1 volatile重排序规则

volatile重排序规则
volatile禁止重排序场景:
1、第二个操作是volatile写,不管第一个操作是什么都不会重排序
2、第一个操作是volatile读,不管第二个操作是什么都不会重排序
3、第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序

2.2 JMM内存屏障插入策略

  1. 在每个volatile写操作的前面插入一个StoreStore屏障
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障
  4. 在每个volatile读操作的后面插入一个LoadStore屏障
    在这里插入图片描述

2.3 JSR133规范

https://blog.csdn.net/qq_43631716/article/details/114760621

86处理器不会对读-读(LoadLoad)、读-写(LoadStore)和写-写(StoreStore)操作做重排序, 会省略掉这3种操作类型对应的内存屏障仅会对写-读(StoreLoad)操作做重排序,所以volatile写-读操作只需要在volatile写后插入StoreLoad屏障
在这里插入图片描述

三、深入理解内存屏障

3.1 JVM层面的内存屏障

在JSR规范中定义了4种内存屏障:

1、LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

2、LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

3、StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见

4、StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令,其他屏障对应空操作

3.2 硬件层内存屏障

硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。

拿X86平台来说,有几种主要的内存屏障:

1、lfence,是一种Load Barrier 读屏障

2、sfence, 是一种Store Barrier 写屏障

3、mfence, 是一种全能型的屏障,具备lfence和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等指令。

内存屏障有两个能力:
1、阻止屏障两边的指令重排序
2、刷新处理器缓存/冲刷处理器缓存


Load Barrier(Ifence)来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;

Store Barrier(sfence)来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。


Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。


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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值