关于java:指令重排序内存屏障很难看完这篇你就懂了

43次阅读

共计 2056 个字符,预计需要花费 6 分钟才能阅读完成。

据说微信搜寻《Java 鱼仔》会变更强哦!

本文收录于 JavaStarter,外面有我残缺的 Java 系列文章,学习或面试都能够看看哦

面试官在问到多线程编程的时候,指令重排序、内存屏障常常会被提起。如果你对这两者有肯定的了解,那这就是你的加分项。

(一)什么是指令重排序

为了使处理器外部的运算单元能尽量被充分利用,处理器可能会对输出的代码进行乱序执行优化,处理器会在计算之后将乱序执行的后果重组,并确保这一后果和程序执行后果是统一的,然而这个过程并不保障各个语句计算的先后顺序和输出代码中的程序统一。这就是指令重排序。

简略来说,就是指你在程序中写的代码,在执行时并不一定依照写的程序。

在 Java 中,JVM 可能依据处理器个性(CPU 多级缓存零碎、多核处理器等)适当对机器指令进行重排序,最大限度施展机器性能。

Java 中的指令重排序有两次,第一次产生在将字节码编译成机器码的阶段,第二次产生在 CPU 执行的时候,也会适当对指令进行重排。

(二)复现指令重排序

光靠说不容易看出景象,上面来看一段代码,这段代码网上呈现好屡次了,但的确很能复现出指令重排序。我把解释放在代码前面。

public class VolatileReOrderSample {
    // 定义四个动态变量
    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;
            // 开两个线程,第一个线程执行 a =1;x=b; 第二个线程执行 b =1;y=a
            Thread thread1=new Thread(new Runnable() {
                @Override
                public void run() {
                    // 线程 1 会比线程 2 先执行,因而用 nanoTime 让线程 1 期待线程 2 0.01 毫秒
                    shortWait(10000);
                    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();
            // 等两个线程都执行结束后拼接后果
            String result="第"+i+"次执行 x ="+x+"y="+y;
            // 如果 x = 0 且 y =0,则跳出循环
            if (x==0&&y==0){System.out.println(result);
                break;
            }else{System.out.println(result);
            }
        }
    }
    // 期待 interval 纳秒
    private static void shortWait(long interval) {long start=System.nanoTime();
        long end;
        do {end=System.nanoTime();
        }while (start+interval>=end);
    }
}

这段代码尽管看着长,其实很简略,定义四个动态变量 x,y,a,b,每次循环时让他们都等于 0,接着用两个线程,第一个线程执行 a =1;x=b; 第二个线程执行 b =1;y=a。

这段程序有几个后果呢?从逻辑上来讲,应该有 3 个后果:

当第一个线程执行到 a = 1 的时候,第二个线程执行到了 b =1,最初x=1,y=1

当第一个线程执行完,第二个线程才刚开始,最初x=0,y=1

当第二个线程执行完,第一个线程才开始,最初x=1,y=0

实践上无论怎么样都不可能 x =0,y=0;

然而当程序执行到几万次之后,居然呈现了 00 的后果:

这就是因为指令被重排序了,x= b 先于 a = 1 执行,y= a 先于 b = 1 执行。

(三)通过什么形式禁止指令重排序?

Volatile 通过 内存屏障 能够禁止指令重排序,内存屏障是一个 CPU 的指令,它能够保障特定操作的执行程序。

内存屏障分为四种:

StoreStore 屏障、StoreLoad 屏障、LoadLoad 屏障、LoadStore 屏障。

JMM 针对编译器制订了 Volatile 重排序的规定:

光看这些实践可能不容易懂,上面我就用艰深的话语来解释一下:

首先是对四种内存屏障的了解,Store 相当于是写屏障,Load 相当于是读屏障。

比方有两行代码,a=1;x=2;并且我把 x 润饰为 volatile。

执行 a = 1 时,它相当于执行了一次一般的写操作;

执行 x = 2 时,它相当于执行了一次 volatile 的写操作;

因而在这两行命令之间,就会插入一个 StoreStore 屏障(后面是写前面也是写),这就是内存屏障。

再让咱们看表,如果第一个操作是一般写,第二个操作是 volatile 写,那么 表格中对应的值就是 NO,禁止重排序。这就是 Volatile 进行指令重排序的原理。

当初,咱们只须要把下面代码的 x 和 y 用 volatile 润饰,就不会产生指令重排序了(如果你能通过表推一遍逻辑,你就能懂了)。

正文完
 0