并发编程之有序性

程序真的是有序执行的吗?

并不一定

首先来看如下小程序:

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

    public static void main(String[] args) throws InterruptedException {

        for (long i = 0; i < Long.MAX_VALUE; i++) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            CountDownLatch latch = new CountDownLatch(2);

            Thread one = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    x = b;

                    latch.countDown();
                }

            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;

                    latch.countDown();
                }
            });
            one.start();
            other.start();
            latch.await();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            }
        }
    }

}

该小程序会出现如下几种排列组合:

image-20221122234207513

上图可知,只有当x=b,y=a或y=a,x=b先执行时,才会出现x=0,y=0的情况,这也证实了,虽然程序中写的是a=1,b=1先执行,下图中程序结束了循环,侧面验证了两个语句之间有一定概率交换顺序执行

image-20221122234529738

两个语句之间有一定概率交换顺序的执行

为何会存在乱序?

简单来说,是为了提高效率

CPU是为了提高效率而做出的一个优化机制,所以存在乱序操作

指令1会去内存读数据,等待返回,指令2在本地只做一个+1操作,因为寄存器的速度比内存快接近100倍,在指令1去内存读取数据时,因为CPU给每个线程分配的时间片是有限的,所以CPU会尽可能的在有限的时间下做更多的操作,在微观上,那么就会优先执行指令2的操作。

image-20221122235116689

乱序存在的条件?

  • as - if- serial 好像-是-序列化 执行的 看起来像序列化执行,但实际未必是序列化执行的
  • 不影响单线程的最终一致性

思考如下代码小程序,会出现什么问题?

public class T02_NoVisibility {
    private static boolean ready = false;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t = new ReaderThread();
        t.start();
        number = 42;
        ready = true;
        t.join();
    }
}

结论:

  1. 可能会出现线程可见性问题,因为ready没有被volatile所修饰
  2. number = 42与ready = true 有可能会存在指令重排序问题导致语句不是顺序执行

第二种情况可能会导致,number的值为0,原因很简单:主线程中可能存在指令重排序的问题导致ready = true先执行,紧接着执行了run方法,然后接着输出了number的值,此时值为0了,接着最后才会执行主线程的 number 赋值42的操作

对象创建过程,this对象溢出

首先查看如下小程序:

class T {
	int m = 8;
}
T t = new T();

汇编码如下:

image-20221123222301940

new:内存中开辟一块空间,并为m设定初始值为0

invokespecial:使成员变量m的值由0改为8,调用构造器

astore_1:建立关联关系,让引用 t 指向内存中的 m

其余两个指令与目前无关,暂不做要求

思考:如下小程序是否有问题?

public class T03_ThisEscape {

    private int num = 8;

    public T03_ThisEscape() {
        new Thread(() -> System.out.println(this.num)
        ).start();
    }

    public static void main(String[] args) throws Exception {
        new T03_ThisEscape();
        System.in.read();
    }
}

当然是有问题的,首先我们先执行new指令,在内存开辟空间,然后正常来说是先执行invokespecial指令,后执行astore_1指令,但是由于指令重排序的原因,有可能astore_1指令先执行,那么就会先让this与成员变量m建立关联关系,此时m值为0,然后紧接着new Thread后输出了m的值,invokespecial指令还没执行时,m值已经输出为0,并没有得到我们预想中的8。

正常情况应该是已经构建好我们的this后在使用,但是目前这种情况this还没完全构建好,也就是处于一个中间状态,这也叫this的溢出。

拓展思考,上述小程序还有可能发生其它的问题吗?

**不要在构造器中new好一个线程后,并且启动它。**因为指令重排序是有概率发生的,如果上述小程序,指令重排序没有发生,那么会先执行invokespecial指令,由于构造器中线程 使用了 this.m操作,但是此时astore_1指令还没执行,this引用还没有与对象建立关联,就有可能出现空指针的情况。

问题思考:DCL单例是否一定需要加volatile?

先说结论,必须要,原因如下:

image-20221127154508406

  1. 由于CPU底层指令是会有重排序的概率的,首先thread1线程会先去new Mgr04()对象,然后正常来说是先执行invokespecial指令调用构造方法,给它赋具体值,最后调用astore_1指令,使得对象间建立关联关系。
  2. 由于指令重排序的原因,有可能astore_1指令先执行了,此时对象之间建立关联关系,紧接着thread2线程接着去判断INSTANCE是否为空,这时因为对象间已经建立起关联关系了,此时的INSTANCE不为空了,就会直接返回半初始化状态的INSTANCE的值,如果是成员变量的m的话,m的值如果是订单数量100多万数据,因为半初始化状态的原因,导致只拿到了m的初试值0就返回了,那么问题就比较严重了。
哪些指令可以互换顺序?

hanppens-before原则(JVM规定重排序必须遵守的规则)

  • 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
  • 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
  • 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。
  • 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。
  • 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断 。
  • 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
  • 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C
如何阻止指令重排序?

intel : lfence sfence mfence(CPU特有指令)

  • CPU级别底层使用内存屏障,内存屏障也是一条特殊的指令,将它加在指令1与指令2之间时,指令1 与 指令2则不能互换顺序

    JVM是一个规范,hotspot是其中实现之一。JVM虚拟机都应该实现自己JVM级别的内存屏障 JVM虚拟机级别的内存屏障与CPU级别内存屏障没有关系

    JVM内存屏障,如下图:

    image-20221127172848499

JVM级别阻止乱序执行必须实现四种功能,也就是LoadLoad、LoadStore、StoreStore、StoreLoad四种读写屏障

volatile在JVM级别规定的比较保守,前后加了四个屏障。Hotspot的实现就是使用了lock;addl的前缀指令去实现的

LOCK 用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内存屏障的作用。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

连梓豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值