Java内存模型基础学习(一)——简单聊聊重排序

前言

前面学习了Java多线程的基础,但也仅仅是基础中的基础,如果想要较系统的学习好Java高并发的相关内容,依旧路漫漫。后续几篇博客会总结JMM的相关内容,总结完成之后再学习JUC的相关内容

Java内存模型简介

众所周知,Java是一次编译,多处运行,如果没有统一的内存模型,是很难做到这一点的。比如C语言,C语言中并不存在内存模型的概念,其有很多都依赖于处理器本身,可能一段相同的C语言程序,在不同处理器上运行,得到的结果都不同,这就很难保证并发安全。而Java语言通过提出Java内存模型的规范,屏蔽了这个问题,让多线程的运行结果可预期。这个Java内存模型的规范就称为JMM规范,各个JVM的实现都遵循JMM规范,以便于开发者可以利用这些规范,更方便的开发多线程程序。

如果没有JMM规范,同一段程序,可能经过了不同的JVM的不同重排序规则之后,在不同的虚拟机上运行的结果不同,这是很存在问题的。

同时,volatile,synchronized,Lock等关键字的底层原理其实都是遵循JMM的,如果没有JMM,则需要我们自己手动指定内存栅栏,这就相当麻烦了,有了JMM之后,我们只需要用同步工具类和关键字即可开发出并发程序。

JMM中最重要,最关键的内容,就是重排序,可见性和原子性。后就简单探讨三者。

重排序

从一个实例出发

先上代码

/**
 * autor:liman
 * createtime:2021/10/8
 * comment:重排序的实例
 * 直到达到某个条件才停止
 */
@Slf4j
public class OutOfOrderExecution {

    //静态的四个变量
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;


    public static void main(String[] args) throws InterruptedException {
        //这里利用CountDownLatch只是为了同时启动线程1和线程2
        CountDownLatch countDownLatch = new CountDownLatch(1);
        int count = 0;
        for (; ; ) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            //线程1,a=1,x=b
            Thread oneThread = new Thread(() -> {
		  try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                a = 1;
                x = b;
            });
            //线程2,b=1,y=a
            Thread twoThread = new Thread(() -> {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                b = 1;
                y = a;
            });
            oneThread.start();
            twoThread.start();
            countDownLatch.countDown();
            oneThread.join();
            twoThread.join();

            //达到指定的条件,才停止
            if(canComplete(x,y)){
                log.info("第:{}次,a:{},b:{},x:{},y:{}", count,a, b, x, y);
                break;
            }
            log.info("第:{}次,a:{},b:{},x:{},y:{}",count, a, b, x, y);
            count++;
        }
    }

    //输出指定条件下的值
    public static boolean canComplete(int x,int y){
        if(x ==1 && y == 1){
            return true;
        }
        return false;
    }
}

如果只考虑线程1 和线程2之间的交替运行,则结果大致会如下。其实具体分析的话,也就是四行代码之前的交替(如果按照线程间代码的交替执行进行分析,一般会有如下结果)

代码执行顺序 最终结果
a=1;x=b;b=1;y=a x=0,y=1
b=1;y=a;a=1;x=b x=1,y=0
b=1;a=1x=b;y=a x=1,y=1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KH5s3F7L-1635238323805)(E:\blogPic\sync&JUC-learn\image-20211026143936251.png)]

但是还有可能存在x=0,y=0的情况

Java内存模型基础学习(一)——简单聊聊重排序

经过分析,应该不会出现x,y都为0的情况,如果出现x=0,y=0大概率就出现了重排序

比如,y=a;a=1;x=b;b=1。线程2中发生了重排序。

什么是重排序

从上面的实例来看,在线程中的两行代码的实际执行顺序和代码最终的运行顺序似乎不同,最后运行的指令代码并没有严格遵循我们的代码语句顺序执行,这就是重排序,上述实例出现了变量x和变量y都为0的情况,其实就是因为发生了指令重排序

指令重排序其实是一种优化操作,通常发生在编译阶段。比如如下指令,在编译阶段指令重排序之后,对计算机而言,逻辑不变的情况下,可一定程度上提升运行效率

Java内存模型基础学习(一)——简单聊聊重排序

左边是重排序之前,右边是重排序之后,重排序之后,指令与CPU寄存器的交互次数有了明显的降低。

除了编译阶段优化,CPU在指令运行层面也可能会对指令进行相关的优化。

关于重排序,我们并不能做什么,这些都是编译层面的优化,只是帮助我们后续完整的理解一下JMM

小结

本篇博客较为简单,内容不多,算是Java内存模型学习的开篇,只是简单介绍了重排序,下一篇博客后续会介绍可见性。

上一篇:JMM(java内存模型)了解


下一篇:JDK成长记13:(深度好文)你能从3个层面分析volatile底层原理么?(上)