写点什么

指令重排序、内存屏障很难?看完这篇你就懂了!

用户头像
Java鱼仔
关注
发布于: 2021 年 01 月 02 日

听说微信搜索《Java 鱼仔》会变更强哦!

本文收录于https://github.com/OliverLiy/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 修饰,就不会发生指令重排序了(如果你能通过表推一遍逻辑,你就能懂了)。


发布于: 2021 年 01 月 02 日阅读数: 61
用户头像

Java鱼仔

关注

你会累是因为你在走上坡路 2020.12.26 加入

微信搜索《Java鱼仔》

评论

发布
暂无评论
指令重排序、内存屏障很难?看完这篇你就懂了!