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

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

听说微信搜《Java鱼仔》真的可以变强哦!


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


(一)什么是指令重排序


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


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


在Java中,JVM能够根据处理器特性(CPU多级缓存系统、多核处理器等)适当对机器指令进行重排序,最大限度发挥机器性能。


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


(二)复现指令重排序


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


publicclassVolatileReOrderSample {
//定义四个静态变量privatestaticintx=0,y=0;
privatestaticinta=0,b=0;
publicstaticvoidmain(String[] args) throwsInterruptedException {
inti=0;
while (true){
i++;
x=0;y=0;a=0;b=0;
//开两个线程,第一个线程执行a=1;x=b;第二个线程执行b=1;y=aThreadthread1=newThread(newRunnable() {
@Overridepublicvoidrun() {
//线程1会比线程2先执行,因此用nanoTime让线程1等待线程2 0.01毫秒shortWait(10000);
a=1;
x=b;
                }
            });
Threadthread2=newThread(newRunnable() {
@Overridepublicvoidrun() {
b=1;
y=a;
                }
            });
thread1.start();
thread2.start();
thread1.join();
thread2.join();
//等两个线程都执行完毕后拼接结果Stringresult="第"+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纳秒privatestaticvoidshortWait(longinterval) {
longstart=System.nanoTime();
longend;
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修饰,就不会发生指令重排序了(如果你能通过表推一遍逻辑,你就能懂了)。



相关文章
|
13天前
汇编语言(第四版) 实验一 查看CPU和内存,用机器指令和汇编指令编程
汇编语言(第四版) 实验一 查看CPU和内存,用机器指令和汇编指令编程
|
2月前
|
编译器 C++
理解内存序,指令重排与内存模型
理解内存序,指令重排与内存模型
57 0
|
2月前
|
存储
【汇编】数据在哪里?有多长、div指令实现除法、dup设置内存空间
【汇编】数据在哪里?有多长、div指令实现除法、dup设置内存空间
|
2月前
|
存储 程序员 数据处理
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
280 1
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
|
11月前
|
缓存 安全 Java
volatile底层的实现原理:volatile关键字的作用、内存模型、JMM规范和CPU指令
volatile底层的实现原理:volatile关键字的作用、内存模型、JMM规范和CPU指令
130 0
|
12月前
持久内存指令(PMDK)简介
持久内存指令(PMDK)简介
273 0
|
12月前
|
存储 安全
4.3 x64dbg 搜索内存可利用指令
发现漏洞的第一步则是需要寻找到可利用的反汇编指令片段,在某些时候远程缓冲区溢出需要通过类似于`jmp esp`等特定的反汇编指令实现跳转功能,并以此来执行布置好的`ShellCode`恶意代码片段,`LyScript`插件则可以很好的完成对当前进程内存中特定函数的检索工作。在远程缓冲区溢出攻击中,攻击者也可以利用汇编指令`jmp esp`来实现对攻击代码的执行。该指令允许攻击者跳转到堆栈中的任意位置,并从那里执行恶意代码。
137 0
|
存储 缓存 索引
通过地址和索引实现数组、CPU指令执行过程、内存概述及内存物理结构
通过地址和索引实现数组、CPU指令执行过程、内存概述及内存物理结构
74 0
|
前端开发 rax
实验一:查看CPU和内存,用机器指令和汇编指令编程
实验一:查看CPU和内存,用机器指令和汇编指令编程
168 0
|
存储 缓存 数据库
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
102 0
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽