《多线程高并发一》CPU的高速缓存架构

CPU的高速缓存架构

多核CPU

多核处理器把多个CPU(核心)集成到单个集成电路芯片(integrated circuit chip)中。一个双核的CPU有2个中央处理单元,所以2个不同的进程可以分别在不同的核心同时执行,大大加快了系统的速度。由于2个核心都在一个芯片上,因此它们之间的通信也要更快,系统也会有更小的延迟。

  1. CPU的物理个数由主板上的插槽数量决定,每个CPU可以有多核心,每核心可能会有多线程
  2. 多核CPU的每核(每核都是一个小芯片),在OS看来都是一个独立的CPU。
  3. 对于超线程CPU来说,每核CPU可以有多个线程(数量是两个,比如1核双线程,2核4线程,4核8线程),每个线程都是一个虚拟的逻辑CPU(比如windows下是以逻辑处理器的名称称呼的),而每个线程在OS看来也是独立的CPU。
    这是欺骗操作系统的行为,在物理上仍然只有1核,只不过在超线程CPU的角度上看,它认为它的超线程会加速程序的运行
    在这里插入图片描述
  4. 多线程没有提供真正意义上的并行处理,每核CPU在某一时刻仍然只能运行一个进程,因为线程1和线程2是共享某核CPU资源的。可以简单的认为每核CPU在独立执行进程的能力上,有一个资源是唯一的,线程1获取了该资源,线程2就没法获取。
  5. 多CPU之间共享内存
    在这里插入图片描述
  • 多线程的优化:合理利用CPU,使用多线程并行取代单线程串行,可以显著提升系统性能。(也可以理解压榨CPU,让CPU一个人并行的干多个人的活,CPU的执行效率非常快,那感情好,不能让它停着,那就给他增加工作量)
  • 多线程线程安全问题,本质上就是多CPU与多CPU内部的多线程之间的资源共享问题。
    • 如果多线程之间彼此独立,互不影响,这种情况是不存在线程安全问题的。
    • 如果多线程之间涉及共享资源,此时处理不当,就存在线程安全问题。
    • 举个简单的例子:A、B、C三人各自有一块蛋糕,他们之间不存在竞争关系,不会出现矛盾;A、B、C共享一块蛋糕,则他们之间存在竞争关系,会出现矛盾。

什么是CPU多级缓存模型?

在这里插入图片描述
当使用一个程序,程序从硬盘中加载到内存当中,然后CPU将程序中的下一条指令地址读取到PC中,然后将相关数据存储到Registers(寄存器)中

  1. PC (Program Counter 程序计数器):记录这一个地址存放下一条执行的指令在哪里,cpu执行完一条就去内存取下一条。
  2. Register寄存器 : 执行指令中的数据放到CPU执行,存储数据的就是寄存器
  3. ALU(Arithmetic Logic Unit)运算单元 : 数据放到寄存器之后使用运算单元alu来运算,运算完写回到寄存器,寄存器再写回到内存里面去。

但是存在一下问题: CPU的速度比内存快100倍

  1. 【CPU速度】CPU的速度非常快,CPU的速度大概是内存速度的100倍。从运算的单元到自己CPU内部存储器中去一个数据

  2. 【内存速度】内存也有速度,严格来说是从运算的单元访问到我们内存

现在有一条指令 内存读一条数据100s ,CPU寄存器进行运算单元计算需要1s

第一,先从内存中取一条数据

第二,通过CPU里面的寄存器对改数据进行 i++ 操作

这就导致了内存速度先执行完指令一才能继续执行CPU的速度的执令二,浪费100个等待时间,如何提升存储数据的速度呢?

因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存,这样会使CPU花费很长时间等待数据到来或把数据写入内存。

在在CPU内部建立位于CPU与内存之间的临时存储器(缓存),它的容量比内存小的多但是交换速度却比内存要快得多。

这样CPU在执行的时候就可以先从高速缓存去找,如果找到之后就里面送到CPU去处理,如果没有找到,,就用相对慢的速度从内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。

高速缓存考虑到CPU运行的数据大小与距离,操作系统把高速缓存拆分为3个缓存 L1 L2 L3 这样数据传输就类似与jvm的双亲委派。
在这里插入图片描述

Register<1ns
L1Cache约1ns
L2Cache约3ns
L3Cache约15ns
main memory约80ns

了解CPU的高速缓存,那么CPU高速缓存是怎么进行传输的呢?缓存行

缓存行概念

在这里插入图片描述
左边是一整颗CPU,它有两个核,每个核它能运行一个线程 。每个核里面都有L1、L2,一颗CPU里面的多个核里面共享L3,多颗CPU去共享整个内存。

现在加入在内存中有一个数据X,CPU如何把他读到寄存器里面进行运算呢?

CPU首先会先从L1缓存中去找,找不到然后去L2缓存里面去找,如果在找不到就去L3缓存找,此时,还是找不到CPU直接从内存里面去读这个X,然后把他在三个缓存里面各复制一份,然后再下次执行的时候就可以快速找到。如果这个时候来了个Y变量,也需要读到CPU里面,通过许多次复制会导致效率不高,CPU把XY统一打包成缓存行一起读到高速缓存中。(缓存行的大小受命中率与复制速度限制)
综合下来,目前缓存行的大小是64个字节。
缓存行图片
并不是所有数据都会被缓存,比如一些较大的数据,缓存行无法容下,那么就只能每次都去主内存中读取

但是这样还好引起缓存伪共享的情况?

缓存行伪共享

上述的缓存行图片 ,现在有两个线程,第一个线程再左边的CPU运行,第二个线程再右边的CPU运行,第一个线程用到的数据是X,第二个线程用到的数据是Y。

这样会产生一个问题:如果X,Y在同一个缓存行里面,第一个CPU只用X,第二个CPU只用Y,现在CPU都会把XY加入到缓存里面,现在如果X改了需要通知其他CPU整个缓存行被改过了,这个缓存行已经是Invalid状态,需要重新读一遍缓存行,现在第二个CPU已经改完后,它改动了Y,第一个CPU不需要去读Y,但是第一个CPU还需要去读一下缓存行,两个互相无关的值变来变去的时候,内部会产生缓存行互相影响问题 (效率低)。

伪共享:位于统一缓存行的两个不同的数据,被两个不同的CPU锁定,产生相互的影响伪共享问题

示例:现在有一个arr数组,里面存放这个两个T对象,对象里面值有一个Long类型,现在线程1频繁的在对象0中的X进行运算操作,线程2频繁的在对象1中的X进行操作。执行如下:

package com.tde.demo.缓存行;

public class Cacheline_nopadding {

    public static class T{
        //8字节  “java中long类型占用8个字节。
        private volatile long x = 0L;
    }
    private static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for(long i = 0;i < 1000_0000L;i++){
                //volatile的缓存一致性协议MESI或者锁总线,会消耗时间
                arr[0].x = i;
            }
        });

        Thread thread2 = new Thread(()->{
            for(long i = 0;i< 1000_0000L;i++){
                arr[1].x = i;
            }
        });
        long startTime = System.nanoTime();
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000);
    }
}

在这里插入图片描述

  1. 解决方法1 : 对齐填充 p1,p2,p3,p4,p5,p6,p7;
package com.tde.demo.缓存行;

public class Cacheline_nopadding {

    private static class Padding{
        //7*8字节
        public volatile long p1,p2,p3,p4,p5,p6,p7;
    }
    public static class T extends Padding{
        //8字节
        private volatile long x = 0L;
    }
//    public static class T{
//        //8字节  “java中long类型占用8个字节。
//        private volatile long x = 0L;
//    }
    private static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for(long i = 0;i < 1000_0000L;i++){
                //volatile的缓存一致性协议MESI或者锁总线,会消耗时间
                arr[0].x = i;
            }
        });

        Thread thread2 = new Thread(()->{
            for(long i = 0;i< 1000_0000L;i++){
                arr[1].x = i;
            }
        });
        long startTime = System.nanoTime();
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000);
    }
}

在这里插入图片描述
效率大大提高

  1. 解决方法2 : 在变量上加上@sun.misc.Contended 默认情况下此注解是无效的,需要在JVM启动时设置-XX:-RestrictContended。

基于高速缓存的存储交互很好的解决了CPU和内存的速度的矛盾,但也引入了一个新的问题 缓存一致性,在多处理器(CPU)系统中,每个CPU都有自己的高速缓存,而他们又共享同一主内存,当多个处理器运算任务都涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致 了解决这个问题,需要各个处理器在访问内存时,需要遵循一些协议,例如MSI、EMSI、MOSI等

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值