操作系统篇-cpu

以下针对java说明

汇编语言(机器语言)的执行过程

计算机通电 -> CPU读取内存中程序(电信号输入)->时钟发生器不断震荡通断电 ->推动CPU内部一步一步执行(执行多少步取决于指令需要的时钟周期)->计算完成->写回(电信号)->写给显卡输出(sout,或者图形)

cpu为什么需要时钟发生器:
时钟是为了同步CPU中各种门电路。

超线程

在这里插入图片描述
四核八线程 其实就是一个ALU对应2个寄存器(registers),平时在读取数据的时候,就会把一个线程相关的数据存储在寄存器里,指令地址存储在PC里面,之后ALU对数据进行计算,如果CPU时间片到了切换线程的时候,就可以把寄存器直接切换到下一个寄存器里进行下一个数据的运算。而不用像只有一个寄存器的时候,在切换线程的时候,需要先把当前寄存器的数据存回内存。

CPU缓存的结构

下图2个cpu,1个cpu2核
在这里插入图片描述
L3有的也有在主板上,离cpu很近

缓存行

在读取数据的时候是按块读取,这些块,在缓存的领域被称作缓存行
程序局部性原理,可以提高效率
充分发挥总线CPU针脚等一次性读取更多数据的能力

缓存行越大,局部性空间效率越高,但读取时间慢
缓存行越小,局部性空间效率越低,但读取时间快
取一个折中值,目前多用:64字节

缓存一致性协议

读取数据会先把数据读到缓存行,如果有一个数据同时被两个线程访问,其中一个cpu修改了数据,这个时候会触发MESI Cache数据一致性协议(intel cpu),会使另一个线程的缓存行失效,另一个数据会从新从内存中读取新的数据。
简单的讲,就是给缓存行标记四种状态,分别是Modified(被修改)、Exclusive(独享的)、Shared(共享的)、Invalid(失效的)。根据不同的状态进行操作达成数据的一致性。
但是有的数据,一个缓存行无法装下,这个时候如果要保持数据一致性,需要锁总线。
跨多个缓存行的数据必须使用总线锁
在这里插入图片描述

缓存行对齐

缓存行对齐:对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享(多个线程同时读取到了同一缓存行时,为了使线程之间的可见性,会使用volatile关键字,使其他线程的缓存行失效进而从内存中从新读取数据,进而造成性能的降低),可以使用缓存行对齐的编程方式

JDK7中,很多采用long padding提高效率

JDK8,加入了@Contended注解,需要加上:JVM -XX:-RestrictContended

开源项目例子:disruptor 高性能并发队列
java 一个long 8个字节,p1~p7加上插入的一个p,就是64字节,对应一个缓存行
在这里插入图片描述

乱序执行

CPU在进行读等待的同时执行其他指令,是CPU乱序的根源,不是乱,而是提高效率。

乱序执行可能会产生的问题

经典例子:DCL(Double Check Lock)单例为什么要加volative?

public class Singleton {
    
    
	// 加volatile禁止指令重排序
    private volatile Singleton singleton;
    // 构造函数是private,防止外部实例化
    private Singleton() {
    
    }
    public static Singleton getInstance() {
    
    
        if (singleton == null) {
    
     // 第一次检查
            synchronized (Singleton.class) {
    
    
                if (singleton == null) {
    
     // 第二次检查,"double check"的由来
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

在这里插入图片描述
new指令不是原子:
new的时候只是在内存申请一个空间并在栈顶压入了指向这段内存的地址(即引用),只有执行完invokespecial后才把m赋值为8。
dup,即 duplicate,其作用就是复制之前分配的new 空间的引用并压入栈顶,为什么需要这个是因为invokespecial 通过#3这个常量池入口找到构造方法,需要知道是谁的构造方法,所以需要消耗一个引用才能执行构造方法,然后弹出栈。
astore_1:调用astore_1将此时的栈顶值弹出存入局部变量中第1的位置(第0位置是this)。

所以,在new一半的时候,可能会发生指令重排,所以DCL(Double Check Lock)单例要加volative
在这里插入图片描述

CPU层面如何禁止乱序?

内存屏障
对某部分内存做操作时前后添加屏障,屏障前后的操作时不可以乱序执行的。
在这里插入图片描述
Intel cpu
底层实现:原语,汇编指令(mfence lfence sfence) 或者总线锁

sfence,写屏障,在sfence指令前的写操作当必须在sfence指令后写操作前完成。
lfence,读屏障,在lfence指令前的读操作当必须在lfence指令后的读操作之前完成。
mfence,读写屏障,在mfence执行之前的读写操作必须在mfence指令后的读写操作之前完成。

intel lock指令
是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个cpu。

JVM规范禁止乱序

JSR内存屏障:
在这里插入图片描述
以下是JVM规范volatile实现,但是在hotspot里,是通过 lock addl $0x0,(%rsp)实现。

StoreStoreBarrier //写写屏障
volatile 写操作
StoreLoadBarrier //写读屏障

LoadLoadBarrier
volatile读操作
LoadStoreBarrier

8个hanppens-before原则:在这里插入图片描述

as-if-serial : 不管硬件什么顺序,单线程执行的结果不变,看上去像是按顺序。
比如在一个单线程里面
x=1
y=2
换为
y=2
x=1
单线程执行的结果不变,看上去就像是按顺序执行。

猜你喜欢

转载自blog.csdn.net/qq_33873431/article/details/109018238