本系列将开启操作系统的学习,本系列的目标是介绍整个操作系统的大体脉络,认识操作系统的结构本系列的目标是开卷有益,不求甚解!作为本系列的开篇,本篇将回忆简单的计组,介绍cpu的内存屏障、指令重排、volatile源码实现======
简单计组知识
计算机基础结构
- 我们可以看见,靠近CPU的总线叫做系统总线,靠近内存的总线叫做IO总线
- CPU中的缓存一共有2层,一个是L1,一个是L2
CPU执行指令过程
- CPU会读取PC中的指令,这个指令的意思是将s++,于是CPU就会将s读进寄存器
- 接下来逻辑计算单元ALU会将s从寄存器中读出来然后执行PC中的指令,计算完毕后会被放回到寄存器
- 最后会把寄存器中的数据重新写回到内存中
线程切换与超线程
- 上图是一个典型的单核单线程结构,一个核心中只能保存一个线程的信息,如果我们要进行线程切换话就要把这些线程的信息单独保存到一个地方,然后再把其他线程拿过来执行,需要切换线程就需要不断地保存、读取数据
- 从这里我们可以感受到线程切换的代价其实不低,如果我们的机器起了太多线程,那么我们的cpu性能就会空耗在线程切换上面
- 这种cpu就是典型的单核双线程,我们可以看到,这个cup中有两组寄存器和pc,也就是说这个cpu可以同时保存两个线程的信息,当这个cpu要切换线程的时候只需要通过上下文转换(context switch)就可以实现线程的切换,不需要把寄存器和pc中的信息保存一份,这就是超线程
存储器层次结构
UMA(Uniform Memory Access—统一内存访问)结构与内存争用
- 这是以前的cpu结构,所有的cpu一起直接共享整块内存,但是我们知道,因为缓存行的概念,不同的cpu之间对于这个缓存会存在争用现象,经过测算,当cpu的数量为4个的时候比较适合,再多的话cpu的资源就会有大部分被浪费在内存争用上
- 为了解决这个问题就提出了NUMA结构
NUMA结构
- NUMA——Non Uniform Memory Access,非统一内存访问,这里面cpu不再是公平地访问每一块儿内存,每一组cpu(一般一组是4颗cpu)有离自己比较近的一块内存,访问内存的时候,优先访问自己的那一块儿内存
指令重排序
指令重排序来源
- 我们在上面的复习中已经感受到了,cpu的执行速度比起从内存中拿去数据执行要快得多,将近快100倍以上,如果我们的cpu在执行指令的时候每一条指令都要完全执行完毕再去执行下一条指令,那么最有可能发生的事情就是cpu会浪费大量的资源在等待上一条指令完成上面
- 这个时候,如果我们cpu发起了一条从内存中读取数据的指令,接下来的指令,只要和第一条指令没有强依赖性,我们就可以优先执行,这样cpu的效率可以得到极高的提升,利用率也提升了
指令重排序优缺点
- 优点:指令重排序可以极大提升cpu的效率
- 缺点:关于指令重排序的缺点,有一个典中典案例就是双重检查单例的volatile,这个讲解太多,不再赘述
CPU与JVM的内存屏障
CPU层级的内存屏障
不同厂家生产的不同CPU的内存屏障都有所不同,因为Intel CPU的普及,我们这里就只介绍 Intel CPU的内存屏障
sfence
存储屏障,当两条指令之间间隔着一条sfence指令的时候,那么这两个指令的存储操作不可以交换顺序
lfence
读取屏障,当两条指令之间间隔着一条lfence指令的时候,那么这两个指令的读取操作不可以交换顺序
mfence
混合屏障,当两条指令之间间隔着一条mfence指令的时候,那么这两个指令的读取操作和存储操作都不可以交换顺序
JVM层级的内存屏障
JVM层级的内存屏障,与其说是一种实现,倒不如说是一种规范,它规范了不同的JVM内存屏障需要做到的规范
LoadStore
两条指令之间如果存在LoadStore屏障的话,那么屏障前面的Load指令和屏障后面的Store指令之间不能出现指令重排
StoreLoad
两条指令之间如果存在StoreLoad屏障的话,那么屏障前面的Store指令和屏障后面的Load指令之间不能出现指令重排
LoadLoad
两条指令之间如果存在LoadLoad屏障的话,那么屏障前面的Load指令和屏障后面的Load指令之间不能出现指令重排
StoreStore
两条指令之间如果存在StoreStore屏障的话,那么屏障前面的Store指令和屏障后面的Store指令之间不能出现指令重排
虽然JVM提供了这么多的内存屏障指令,但是他们的底层实现却不一定也是CPU的内存屏障,这里我们可以从volatile的源码中看出端倪
volatile源码实现与sync的对比
volatile与sync比较
- volatile可以防止指令重排序,sync不能
- volatile不能保证原子性,sync可以
- volatile可以保证线程之间可见性,sync也可以
volatile的字节码层次实现
- 我们以前解析过volatile标记变量的字节码,发现其实只是在这个变量的access_flag上添加了一个volatile标记,并没有在JVM的汇编指令中看到什么端倪
sync的字节码层次实现
- sync的字节码有两种实现
- sync标记在方法的头上,这时我们无法从JVM汇编指令上看出端倪,只是在access_flag上添加了一个sync标记
- sync标记在方法里面,作为一个同步代码块里的时候,我们就可以在汇编指令上看到命令了,这个命令就是monitor
volatile在JVM层次实现
- volatile在虚拟机层次的实现其实就是调用了JVM提供的内存屏障指令
sync在JVM层次实现
- JVM层次的实现其实最终就归结到了被sync锁住的对象身上,这个对象被锁住的关键点就在于这个对象的对象头有3位可以用于标识锁状态,其中有2位基本是专门为了标识sync锁存在的,根据这2位就可以判断sync锁的状态
volatile的底层实现
volatile的底层其实并不是我们大家所想象的使用cpu内存屏障,而是使用的lock指令,我们等会详解,现在先解释为什么使用的内存屏障
首先,如果使用内存屏障的话,效率一定是要高于lock指令的,但是内存屏障不同的cpu会不同,如果非要针对每一种cpu定制一种volatile实现的话,那么就非常的麻烦;与之相比,lock指令基本是每一种cpu都有的,而且实现都差不多,所以volatile直接就调用lock指令来实现
lock锁,也就是我们平时说的总线锁,这个锁的粒度极大,一锁就是整条总线。当cpu接收到lock信号的时候就会将总线锁死,让其他cpu无法使用内存。
lock addl $0×0,(%esp)
这条指令就是volatile的cpu原语,我们可以发现,这条指令就是往esp寄存器中加一个0,相当于没有变化,但是这里面重要的不是这个,而是前面的lock指令,目的是为了把总线锁上
sync的底层实现
sync的底层实现其实是一条汇编
lock cmpxchg
我们注意的重点不是lock,虽然lock也非常重要,但是我们要理解sync还是需要理解cmpxchg指令。
cmpxchg dest,src
将AL、AX、EAX或RAX寄存器中的值与第一个操作数dest(目标操作数)进行比较。 如果两个值相等,则将第二个操作数src(源操作数)加载到目标操作数中。 如果不相等,则目标操作数被加载到AL、AX、EAX或RAX寄存器中。 RAX寄存器仅在64位模式下可用。
该指令可以与LOCK锁前缀一起使用,使得指令以原子的方式执行。 为了简化到处理器总线的接口,不管比较结果是否相等,目标操作数都将接收一个写周期。 如果比较失败(不相等),则目标操作数将会被回写(为原来的值);否则,源操作数将被写入目标操作数
这就是一个cas操作,通过这种方式就可以实现资源锁定,虽然锁的是总线