【JavaEE初阶】第五节.多线程 ( 基础篇 ) 线程安全问题(上篇)

news2024/9/21 12:43:59

目录

文章目录

前言

一、线程安全的概述

1.1 什么是线程安全问题

1.2 存在线程安全问题的实例

二、线程安全问题及其解决办法 

2.1 案例分析

2.2 造成线程不安全的原因 

2.3 线程加锁操作解决原子性 问题 ;

      2.3.1 什么是加锁

      2.3.2 使用 synchronized关键字 进行加锁

      2.3.3 synchronized 使用示例 

三、Java标准库里面的线程安全类

总结


前言

今天我们将进入到线程基础篇当中有关线程安全的问题,线程安全对于我们学习线程有着非常重要的作用;今天我们将通过本节学习,能够更好的认识到线程安全以及解决线程安全的问题;

就让我们进入到今天的学习当中!!!!!!!!


一、线程安全的概述

1.1 什么是线程安全问题

线程安全问题 出现的 "罪魁祸首",正是 调度器的 随机调度 / 抢占式执行 这个过程;

在随机调度之下,多线程程序执行的时候,有无数种可能性,有无数种可能的排列方式;

在这些排列顺序中,有的排列方式 逻辑是正确的,但是有的排列方式 可能会引出 bug;

对于多线程并发时,会使程序出现 bug 的代码 称作线程不安全的代码,这就是线程安全问题;

接下来,举出一个典型的例子,来观察一番 到底什么是线程安全问题;

1.2 存在线程安全问题的实例

创建两个线程,让这两个线程 同时并发 对一个变量,自增 5w 次,最终预期能够一共自增 10w 次;

代码示例:

package thread;
 
class Counter {
    //用来保存计数的变量
    public int count;
 
    public void increase() {
        count++;
    }
}
 
public class Demo14 {
    public static void main(String[] args) {
        //这个实例用来进行累加
        Counter counter = new Counter();
 
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
 
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:" + counter.count);
    }
}

 运行结果:

说明:

很明显,我们可以发现 程序运行的结果 都是 小于 10w 次的,即便是运行多次,结果也都是小于 10w 次;

事实上,正确的结果是,得到的数字 count 在 5w 到 10w 之间;

这是怎么回事呢? 我们接下来慢慢分析;

二、线程安全问题及其解决办法 

2.1 案例分析

按理来说,上述实例 运行的结果 count 应该等于 10w

可是 连续运行多次,就会发现 每一次运行的结果都不一样,但都是小于 10w,这是为什么呢?

这个就是 线程不安全的问题

原因主要是:随机调度的顺序不一样,就导致程序运行的结果不一样;

上述的 bug 是怎么形成的呢?

这个得需要站在硬件的角度来理解:

像 count++ 这一行代码,其实对应的是 三个机器指令:

  1. 首先 需要从内存中读取数据到 CPU,这个指令称为 load指令;
  2. 其次 在 CPU 寄存器中,完成加法运算,这个指令称为 add指令;
  3. 最后 把寄存器的指令 写回到内存中,这个指令称为 save指令;

注意:

这个在 JavaEE初阶 的第一篇文章中提到过,不清楚的可以跳转过去看一看;

这三个步骤,如果是在单线程下执行,那是没有任何问题的

但是如果是在多线程下执行,那就不一定了


现在,我们可以以一条时间轴,来画一下其中常见的情况:

load 是把内存中的数据读到 寄存器里,add是在寄存器里面进行加法操作,save是把寄存器里面的值放回到内存;

情况一:

对两个数进行自增操作,内存 初始值为 0,两个线程进行并发执行,进行两次自增;

寄存器A 表示 线程1 所用的寄存器,寄存器B 表示 线程2 所用的寄存器;

通过上述的执行过程,我们可以看到,两个线程 各自增一次,预期 自增两次,实际上的结果是 2,没有任何问题;

看起来是没有任何问题的,可是实际情况下 这个可是多线程,只是出现无数种情况的其中一种而已,只是这种排列方式恰好没有问题(其他的排列方式就不一定了) 


情况二

对两个数进行自增操作,内存 初始值为 0,两个线程进行并发执行,进行两次自增;

寄存器A 表示 线程1 所用的寄存器,寄存器B 表示 线程2 所用的寄存器;

如上所示,明明在内存里面自增了两次,但是最终内存的值 仍然是 1;

这就是典型的线程不安全导致的 bug;


情况三:

对两个数进行自增操作,内存 初始值为 0,两个线程进行并发执行,进行两次自增;

寄存器A 表示 线程1 所用的寄存器,寄存器B 表示 线程2 所用的寄存器;

如上所示,最终的内存中保存的是 1;

这是典型的线程不安全的问题;


其他的情况:

说明:

由于是多线程,所以有无数种情况 ;

总之,在无数中的排列顺序情况下,只有 "先执行完第一个线程,再执行完第二个线程" 以及 "先执行完第二个线程",再执行完第一个线程 的这两种情况,是没有问题的;

剩下的情况,全部都是和正确结果不匹配;

总结:

回到最初的代码程序,我们就可以知道:

在极端情况下,如果所有的执行排列都是 "先执行完第一个线程,再执行完第二个线程" 以及 "先执行完第二个线程",那么此时的总和就是 10w;

在极端情况下,如果所有的执行排列顺序 是不包括这两种情况的其他情况,那么此时总和就是 5w;

更实际的情况下,调度器具体调度出多少种这两种极端的情况,我们是无法确定的;

因此 最终的结果是 5w ~ 10w !!!!!!

操作系统的随机调度,其实不是 "真随机",而是 操作系统内核的调度器调度线程,其内部是有一套 逻辑 / 算法,来支持这一调度过程 ;

即 每种出现的排列情况下不是均等的,所以不可以通过排列组合的情况下算出每种情况 出现的概率的 ;

2.2 造成线程不安全的原因 

(一)操作系统的 随机调度 / 抢占式执行

这个是 万恶之源、罪魁祸首!!!!!!

这个是 操作系统内核 实现的时候,就是这样设计的,因此 我们改不了(就算可以改得了自己的电脑,也改不了其他的人的那么多电脑),对此 我们是无能为力的


(二)多个线程 修改 同一个变量

如果只是一个线程修改变量,没有线程安全问题!!!

如果是多个线程读同一个变量,也没有线程安全问题!!!

如果是多个线程修改不同的变量,还是没有线程安全问题!!!

但是,多个线程修改同一个变量,那就有了线程安全问题了

所以,在写代码的时候,我们可以针对这个要点进行控制(可以通过调整程序的设计,来去规避 多个线程修改同一个变量

但是,此时的 "规避方法" 是有适用范围的,不是所有的场景都可以规避掉(这个得要看具体的场景)


(三)有些修改操作,不是 原子的修改,更容易触发 线程安全问题

在 MySQL数据库中说过,不可拆分的最小单位 就叫做原子

如:赋值操作来修改(=,只对应一条机器指令),就是视为原子的

像之前通过 ++操作 来修改(对应三条机器指令),就不是原子的


(四)内存可见性 引起的线程安全问题

内存可见性,这个就是另外一个场景了:一个线程写,一个线程读的场景;

这个场景 就特别容易因为 内存可见性 而引发问题;

内存可见性;

线程1:进行反复的 读 和 判断 ;

线程2:在某个环节下进行修改;

如果是正常的情况下,线程1 在读和判断,线程2 突然写了一下 => 这是正常的,在线程2 写完之后,线程1 就能立即读到内存的变化,从而让判断出线变化;

但是,在程序运行过程中,可能会涉及到一个操作 —— "优化" (可能是编译器 javac,也可能是 JVM java,也可能是操作系统 的行为);

那么 由于 线程1 频繁的进行 load   test 操作,就很有可能会被优化成 load   test   test......操作(会认为 一直读的都是一样的值,所以不需要再读了);

 

 

每次 load操作 都是读内存操作,每次 test操作 都是在读寄存器,读内存操作 要比 读寄存器操作 慢上几千倍、上万倍;

正是由于 load操作 读的太慢,再加上 反复读,每一次读到的数据又一样,所以 JVM 就做出了这样的优化,就不再重复的从内存中读了,直接就复用第一次从内存读到寄存器的数据就好了;

那么,如果在优化之后,线程2 突然又写了一个数据;;

由于 线程1 已经优化成读寄存器了,因此 线程2 的修改,线程1 感知不到 =>这就叫做 内存可见性问题(内存改了,但是在 优化 的背景下,读不到、看不见了);

所谓优化是指在执行正确的前提下,来做出变化 使得性能更优;

一定要保证程序的逻辑是正确的,再说效率问题!!!

上述场景的优化,在单线程场景下,没有问题;但是在多线程情况下,就可能会出现问题:多线程环境太复杂,编译器 / JVM / 操作系统 进行优化的时候就可能产生误判;

针对这个问题,Java 引入了 volatile关键字,让程序猿手动的禁止 编译器 / JVM / 操作系统 对某个变量进行上述优化!!! 


(五)指令重排序,也可能引起线程不安全

        

指令重排序,也是 操作系统 / 编译器 / JVM 优化操作!!!

它调整了代码的执行顺序,达到加快速度的效果;

举例说明:

比如说,张三媳妇 要张三去到超市买一些蔬菜,并且给了他一张清单:

  1. 西红柿
  2. 鸡蛋
  3. 茄子
  4. 小芹菜

调整顺序后,也是符合张三媳妇 对张三的要求:买到了四样菜,并且效率也是得到了提高;

至于买的过程是什么样子的,张三媳妇并不关心;

这个就叫做 指令重排序!!!

指令重排序,也会引发线程不安全

如:

此处,就容易出现指令重排序引入的问题:

2 和 3 的顺序是可以调换的;

在单线程下,调换这两的顺序,是没有影响的;但是如果在多线程条件下,那么是会出现 多线程不安全:

假设 另一个线程,尝试读取 t 的引用,如果是按照 2、3的顺序,第二个线程读到 t 为 非null 的时候,此时 t 就一定是一个有效对象;如果是按照 3、2的顺序,第二个线程读到 t 为 非null 的时候,仍然可能是一个无效对象!!!

总结:

线程安全问题出现的五种原因:

前三种原因 是更普遍的;

1.系统的随机调度(万恶之源、无能为力)
2.多个线程同时修改同一个变量(部分规避)
3.修改操作不是原子的(有办法改善的)
后两种原因,是 编译器 / JVM / 操作系统 搞出的幺蛾子(但是 总体上来说还是利大于弊的)

4.内存可见性;
5.指令重排序;
编译器 / JVM / 操作系统 误判了,导致把不应该优化的地方给优化了,逻辑就变了,bug 就出现了(当然,后两种原因 也可以用 volatile关键字 来进行解决)

2.3 线程加锁操作解决原子性 问题 ;

现在先重点来介绍一下 解决线程安全问题出现的第三种原因的方法(原子性)通过 加锁操作,来把一些不是原子的操作打包成一个原子的操作!!!

加锁在 Java 中有很多方式来实现,其中最常用的就是 synchronized(用法其实也挺简单的,我们需要注意的是它的拼写和发音)


2.3.1 什么是加锁

举例说明:

举个简单明了的例子,假设 你要去银行ATM机 取钱(我们都知道,ATM机 是放在一个单独的小房子里面的,每个小房子都有一把锁),如果你进去了,那么这个锁就会自动的锁起来,别人就进不去了,除非是 你已经取钱成功了 并且 自己已经出来了,下一个人才可以继续使用到 ATM机;

取钱成功了,说明 取钱的几个步骤是成功了的,那么我们希望,去 ATM机 取钱的这些步骤,是能够一气呵成的(如果 不一气呵成,万一走的时候忘记啥步骤,取钱没有成功,大大咧咧的走了;后面的人一顿操作猛如虎,把你的钱取走了咋搞)

为了使这些步骤一气呵成,引入的办法就是 加锁;

加锁:

即 在你进去的时候,门就被锁了,其他的人就进不去了;

然后你就可以完成 刷卡、输入密码 等等的操作,等这些操作都完成了之后,再把锁给打开,然后你就可以出去了;

下一个人也就可以进来重复和你一样的操作了;

实际上,银行里面的 ATM机 就是这样设计的;

此时的 "你" 指的就是 "线程","ATM机" 指的就是 "对象","门上的锁" 指的就是 "锁","其他人" 指的就是 "其他的线程";

在 Java中,加锁的方式有很多种,其中最常见的加锁方式就是用 synchronized关键字 进行加锁

2.3.2 使用 synchronized关键字 进行加锁 

synchronized 从字面意思上翻译叫做 "同步",其实 实际上它所起的是 互斥的效果;

在一开始的时候,列举了一个典型的线程不安全的例子:

创建两个线程,让这两个线程 同时并发 对一个变量,自增 5w 次,最终预期能够一共自增 10w 次;

package thread;
 
class Counter {
    //用来保存计数的变量
    public int count;
 
     public void increase() {
        count++;
    }
}
 
public class Demo14 {
    public static void main(String[] args) {
        //这个实例用来进行累加
        Counter counter = new Counter();
 
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
 
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:" + counter.count);
    }
}

那么,怎么使用 synchronized关键字 来解决这个线程不安全的问题呢?

—— 很简单,我们在上面的 increase() 方法 前面加上 synchronized关键字即可(写在 void 之前都可以):

或者:

 此时,我们再执行程序,发现无论再运行多少次,发现运行结果是正确的了:


那么,为什么加锁之后,就可以来实现 线程安全的保障呢? 

LOCK 这个指令是互斥的,当 线程t1 进行 LOCK 之后,t2 也尝试 LOCK,那么 t2 的 LOCK 就不会直接成功!!!

所以说,在加锁的情况下,线程的三个指令就被岔开了,就可以保证 一个线程 save 之后,另一个线程才 load,于是此时的计算结果就准了 

2.3.3 synchronized 使用示例 

(一)synchronized 直接修饰普通方法

public class Demo14 {
    public synchronized void methond() {
 
   }
}

(二)synchronized 修饰静态方法

public class Demo14 {
    public synchronized static void method() {
 
   }
}

(三)修饰代码块

public class Demo14 {
    public void method() {
        synchronized (this) {
            
       }
   }
}

() 里面的 this 指的是:是针对哪个对象进行加锁!!!

加锁操作,是针对一个对象来进行的!!!

我们要重点理解,synchronized 锁的是什么:两个线程竞争的是同一把锁,才会产生阻塞操作(即 两个线程尝试使用两把不同的锁,不会产生阻塞操作)

如:举例说明:

换句话说,1号滑稽 进入1号坑位,只是针对 1号坑位 进行了加锁,别人想要进入 1号坑位,就需要阻塞等待;但是 如果想要进入其他的 空闲坑位,那么则不需要等待

这里的 滑稽老铁 指的就是 线程坑位(的门上的锁,其实就是 synchronized() 括号里面的东西)  指的就是 要加锁的对象

注意:

  1. 在Java里,任何一个对象,都可以用来做 锁对象,即 都可以放在  synchronized() 的括号中;其它的主流语言 都是专门搞了一类特殊的对象,用来作为 锁对象(大部分的正常对象 不能用来加锁)!
  2. 每个对象,内存空间中都会有一个特殊的区域 —— 对象头(JVM自带的,对象的一些特殊的信息)
  3. synchronized 写到普通方法上 相当于是对 this(可创建出多个实例) 进行加锁;
  4. synchronized 写到静态方法上 相当于是对 类对象(整个 JVM 里只有一个) 进行加锁synchronized (类名.class);

三、Java标准库里面的线程安全类

在Java标准库里面,很多线程都是不安全的,如:例如,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder ;

当然,还是有一些是线程安全的,如:Vector (不推荐使用),HashTable (不推荐使用),ConcurrentHashMap (推荐),StringBuffer,String;

需要注意的是,加锁也是有代价的,它会牺牲很大的运行速度(毕竟,加锁涉及到了一些线程的阻塞等待,以及 线程的调度),所以可以视为,一旦使用了锁,我们的代码基本上就和 "高性能" 说再见了


总结

今天有关线程安全问题的上篇就讲到这里,下一节内容我们将继续探讨线程安全的问题,让我们下期再见!!!!!!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/172250.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

爆品跟卖商家必读:2023年快速入局TikTok选品5大关键

TikTok商业进程一直有在发展&#xff0c;开启东南亚小店&#xff0c;美国小店内邀……有吸引了不少外贸工厂和传统跨境电商卖家等玩家入局。2022年这一年&#xff0c;不管是直播带货&#xff0c;短视频带货&#xff0c;还是广告投流&#xff0c;数据都有新的变化。据报道&#…

Word 允许西文在单词中间换行,没用/无效 终极办法

有时在写论文中&#xff0c;英文的调整相当麻烦&#xff0c;为了节约版面&#xff0c;会设置允许西文在单词中间换行。但有时不希望这样&#xff0c;特别是在复制网上英文时&#xff0c;会出现单词分断换行情况&#xff0c;如何解决&#xff1a; 1.一般办法。 在Word选择要调整…

C规范编辑笔记(十)

往期文章&#xff1a; C规范编辑笔记(一) C规范编辑笔记(二) C规范编辑笔记(三) C规范编辑笔记(四) C规范编辑笔记(五) C规范编辑笔记(六) C规范编辑笔记(七) C规范编辑笔记(八) C规范编辑笔记(九) 正文&#xff1a; 又是新的一年&#xff0c;2023年的第一篇没想到隔了这么久…

MyBatis-Plus加密字段查询(密文检索)

MyBatis-Plus数据安全保护(加密解密)解释说明 1.字段加密后&#xff0c;数据库存储的字段内容为十六进制格式的密文2.条件查询时&#xff0c;若不对密文进行处理将无法匹配出想要的结果3.处理方式是借助SQL的AES_DECRYPT函数将密文解密后匹配4.SQL的解密函数只有AES_DECRYPT&am…

Java-流和IO

文章目录流InputStreamFileInputStream常用方法详情代码示例BufferInputStream常用方法详情代码示例OutputStreamFileOutputStream常用方法详情代码示例BufferedOutputStream常用方法详情代码示例ReadWriteJava的java.io库提供了IO接口&#xff0c;IO是以流为基础进行输入输出的…

云原生技能树-docker image 操作-练习篇

从Docker Hub 拉取已有镜像 一个Docker 镜像(image)包含了程序代码和程序运行所依赖的所有环境。 Docker 镜像一般存放在镜像仓库服务(Image Registry)里&#xff0c;默认的镜像仓库服务是Docker Hub。 用户可以制作、构建镜像、将镜像上传到镜像仓库服务&#xff0c;从而可以…

100w人在线的 弹幕 系统,是怎么架构的?

Shopee是东南亚及中国台湾地区的电商平台 。2015年于新加坡成立并设立总部&#xff0c;随后拓展至马来西亚、泰国、中国台湾地区、印度尼西亚、越南及菲律宾共七大市场。 Shopee拥有商品种类&#xff0c;包括电子消费品、家居、美容保健、母婴、服饰及健身器材等。 2022年第二…

【STM32学习】GPIO口的八种工作模式

GPIO口的八种工作模式一、参考资料二、施密特触发器1、电路2、电路计算一、参考资料 GPIO原理图详解 强烈建议观看&#xff1a;GPIO为什么这样设计&#xff1f; 施密特触发器—原理 施密特触发器—计算 什么是运放的虚短和虚断&#xff1f; 二、施密特触发器 关于GPIO的原理与…

JavaWeb-JSP

JavaWeb-JSP 1&#xff0c;JSP 概述 JSP&#xff08;全称&#xff1a;Java Server Pages&#xff09;&#xff1a;Java 服务端页面。是一种动态的网页技术&#xff0c;其中既可以定义 HTML、JS、CSS等静态内容&#xff0c;还可以定义 Java代码的动态内容&#xff0c;也就是 J…

设计模式 (二) 工厂模式 Java

目录 一、案例引出 二、简单工厂模式 二、抽象工厂 工厂设计模式&#xff0c;顾名思义类似一家工厂来制造各种产品&#xff0c;目的在于提高代码的可扩展性。 一、案例引出 通过接口来实现一类产品的功能&#xff0c;如目前有飞机、轮船、汽车这类产品的实体类&#xff0c…

Windows 安装 Android Studio

1、下载Android Studio https://r1—sn-2x3edn7s.gvt1.com/edgedl/android/studio/install/2022.1.1.19/android-studio-2022.1.1.19-windows.exe?cms_redirectyes&mhBy&mip175.146.144.124&mm28&mnsn-2x3edn7s&msnvh&mt1673878346&mvm&mvi1…

Pycharm社区版侧边栏没有database按钮

Pycharm有专业版和社区版这两个版本&#xff0c;普通人在大多数情况下我们都会选择社区版进行下载安装。为啥呢&#xff1f;因为社区版免费&#xff0c;专业版收费一般人用不起呀&#xff0c;而且社区版能够基本满足我们的日常需求&#xff08;但是这也就意味着社区版会比专业版…

LINUX学习之查看文件常用命令(四)

cat 命令描述 cat命令是一种用于查看文件内容的命令&#xff0c;它可以将文件的内容直接输出到标准输出 以下是cat命令常用参数&#xff1a; -A &#xff1a;显示所有字符&#xff0c;包括特殊字符-b &#xff1a;显示行号&#xff0c;仅针对非空白行-E &#xff1a;在每行…

【Java集合进阶】Collection 体系集合详解(ArrayList,LinkedList,HashSet,TreeSet...)

文章目录1. 概念2. 集合和数组的区别3. 集合的体系结构4. Collection父接口5. List 子接口6. List 实现类6.1 ArrayList 类6.2 Vector 类6.3 LinkedList 类6.4 ArrayList和LinkedList的区别7. Set 子接口8. Set 实现类8.1 HashSet 类8.2 TreeSet 类9. Collections 工具类Java编…

Vue9-数据代理

数据代理&#xff1a;通过一个对象代理对另一个对象中属性的操作&#xff08;读写&#xff09; 结论&#xff1a;Vue中通过vm&#xff08;实例对象&#xff09;来代理data中的所有数据 1.首先创建了一个vm对象 2.然后 vue就给vm准备了一些数据&#xff0c;当然也将data存放到…

Ubuntu下源码编译VirtualBox二 —— 源码编译(1)

先打个预防针&#xff1a;在Ubuntu下编译VirtualBox可以说相当复杂。 1. 编译指导 &#xff08;1&#xff09;进入技术文档页面 在VirtualBox主页&#xff08;Oracle VM VirtualBox&#xff09;中&#xff0c;鼠标左键点击“Documentation”下的“Technical docs”&#xff0…

【Android安全】安装mitmproxy Https抓包证书 | 安卓SSL抓包

安装mitmproxy Https抓包证书 macbook上 mitmproxy 抓取安卓手机https流量 重点是安装mitmproxy Https抓包证书 前提 手机需要root&#xff0c;macbook上需要安装好mitmproxy macbook安装mitmproxy 需要完成下文1-3&#xff1a; https://github.com/doug-leith/cydia &…

【内排序 -- 八大排序】

目录&#xff1a;前言算法实现&#xff08;一&#xff09;插入排序1.直接插入排序2.希尔排序&#xff08;缩小增量排序&#xff09;&#xff08;二&#xff09;选择排序1.选择排序2.堆排序&#xff08;三&#xff09;交换排序冒泡排序快速排序1&#xff08;hoare版&#xff09;…

ARM S5PV210的SD卡启动实战

一、S5PV210的SD卡启动实战1 1、任务&#xff1a;大于16KB的bin文件使用 SD 卡启动 (1) 总体思路&#xff1a;将我们的代码分为 2 部分&#xff1a;第一部分 BL1 ≤ 16KB&#xff0c;第二部分为任意大小。 iROM 代码执行完成后&#xff0c;从 SD 卡启动会自动读取 BL1 到 SRA…

多核缓存一致性问题及解决方案MESI协议《深入浅出计算机组成原理》学习笔记 Day 4

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3 文章目录系列文章目录前言一、多核缓存一致性从何而来&#xff08;What&#xff09;二、怎么解…