【JavaEE】CAS原理实现 + 常见应用

news2024/11/15 20:37:10

本文基于jdk8

参考:

黑马程序员深入学习Java并发编程,JUC并发编程全套教程_哔哩哔哩_bilibili


CAS原理

CAS:比较和交换(设置) Compare And Swap(Set)。当A的值为5的时候,给A设置值为10。这里涉及到的比较和设置值的操作是原子的。


CAS的操作系统层实现

操作系统层面的CAS是一条CPU的原子指令(cmpxchg指令),正是由于该指令具备了原子性,因此使用CAS操作数据时不会造成数据不一致的问题。


CAS的Java层实现

Java是把底层C++代码调用的这个原子指令封装到native方法中了。不过还提供了Unsafe类用来操作相关线程等。但这个类无法直接调用,只能通过反射来使用。该类不建议使用

获取Unsafe

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeAccessor {
    static Unsafe unsafe;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    static Unsafe getUnsafe() {
        return unsafe;
    }
}

使用Unsafe

import sun.misc.Unsafe;

import java.lang.reflect.Field;


class Student {
    volatile int id;
    volatile String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public static void main(String[] args) throws NoSuchFieldException {
        Unsafe unsafe = UnsafeAccessor.getUnsafe();
        Field id = Student.class.getDeclaredField("id");
        Field name = Student.class.getDeclaredField("name");
        // 获得成员变量的偏移量
        long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
        long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
        Student student = new Student();
        // 使用 cas 方法替换成员变量的值
        UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
        UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
        System.out.println(student);
    }
}


CAS中的ABA问题

某个共享变量在线程1中刚获取到值为A,后面要对这个值进行CAS操作,把它变成C。

但是线程1在CAS之前,线程2也读到了共享变量的值,并且它先于线程1对改共享变量的CAS操作,把这个共享变量变成B,最后在线程1在CAS操作之前又把这个变量改成A。

当线程1要执行CAS操作时,它一看还是原来的A,就直接执行了,但实际上并不是原来的A了

import java.util.concurrent.atomic.AtomicReference;

import static java.lang.Thread.sleep;

public class CasAba {
    static volatile AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程获取A");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        other();
        sleep(1000);
        // 尝试改为 C
        if (ref.compareAndSet(prev, "C")) {
            System.out.println("主线程修改成功:" + prev + "->" + ref.get());
        }
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            System.out.println("t1线程获取A");
            String prev = ref.get();
            if (ref.compareAndSet(ref.get(), "B")) {
                System.out.println("t1修改成功:" + prev + "->" + ref.get());
            }
        }, "t1").start();

        sleep(500);

        new Thread(() -> {
            System.out.println("t2线程获取B");
            String prev = ref.get();
            if (ref.compareAndSet(ref.get(), "A")) {
                System.out.println("t2修改成功:" + prev + "->" + ref.get());
            }
        }, "t2").start();
    }
}


解决方案

如果比较值不够,那么就在值的基础上在加个版本号。每次读取该值,版本就加一。比较的时候也要看看版本号是不是之前的版本。

AtomicStampedReference

该类就是带版本号的实现。

import java.util.concurrent.atomic.AtomicStampedReference;

import static java.lang.Thread.sleep;

public class AtomicStampedReferenceTest {
    static volatile AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        System.out.println("主线程获取初始值:" + prev + " 版本号:" + stamp);
        other();
        sleep(1000);
        // 尝试改为 C + 版本号加一
        if (ref.compareAndSet(prev, "C", stamp, stamp + 1)) {
            System.out.println("主线程修改成功:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
        } else {
            System.out.println("主线程修改失败:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
        }
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            String prev = ref.getReference();
            int stamp = ref.getStamp();
            System.out.println("t1线程获取初始值" + prev + " 版本号:" + stamp);
            if (ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1)) {
                System.out.println("t1修改成功:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
            }
        }, "t1").start();

        sleep(500);

        new Thread(() -> {
            String prev = ref.getReference();
            int stamp = ref.getStamp();
            System.out.println("t2线程获取初始值" + prev + " 版本号:" + stamp);
            if (ref.compareAndSet(prev, "A", stamp, stamp + 1)) {
                System.out.println("t2修改成功:" + prev + "->" + ref.getReference());
            }
        }, "t2").start();
    }
}


CAS特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,多重试几次

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。


常见应用

以下这些类或者关键字都使用了CAS。大部分都在juc下

synchronized

synchronized在进行锁升级的时候,基本在每次升级的使用都用到了CAS来设置值。

比如在偏向锁时,把线程ID记录到对象头中,这里的记录用的CAS来记录的;

在轻量级锁时,要把对象头中的Mark Word和锁相关的值记录到锁记录中。

上面这些都是单次的设置值的操作,而在轻量级锁在变成重量级锁之前,会自旋等待,这里就一直是CAS操作。


原子基础类型

对于一些基础类型,juc提供了原子类来方便使用。

import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomicInteger {
    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(0);
        // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
        System.out.println(i.getAndIncrement());
        // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
        System.out.println(i.incrementAndGet());
        // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
        System.out.println(i.decrementAndGet());
        // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
        System.out.println(i.getAndDecrement());
        // 获取并加值(i = 0, 结果 i = 5, 返回 0)
        System.out.println(i.getAndAdd(5));
        // 加值并获取(i = 5, 结果 i = 0, 返回 0)
        System.out.println(i.addAndGet(-5));
        // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.getAndUpdate(p -> p - 2));
        // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.updateAndGet(p -> p + 2));
        // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
        // getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
        System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
        // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
    }
}

原子引用类型

可以使用这个类型来把对象的引用给变成原子的。

AtomicReference  AtomicMarkableReference  AtomicStampedReference

比如上面CAS中的ABA问题中的实例,String的使用方法。


原子数组

对于数组的包装:AtomicIntegerArray  AtomicLongArray  AtomicReferenceArray

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class TestAtomicArray {

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数3,自增方法,回传 array, index
     * 参数4,打印数组的方法
     */
    // supplier 提供者 无中生有 ()->结果
    // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) {
        List<Thread> ts = new ArrayList<>();
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {
            // 每个线程对数组作 10000 次操作
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }
        ts.forEach(t -> t.start()); // 启动所有线程
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // 等所有线程结束
        printConsumer.accept(array);
    }


    public static void main(String[] args) {
        demo(   () -> new int[10],
                (array) -> array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array)));

        demo(   () -> new AtomicIntegerArray(10),
                (array) -> array.length(),
                (array, index) -> array.getAndIncrement(index),
                array -> System.out.println(array));
    }
}


原子字段(属性)

对于类中的字段的包装,必须配合volatile使用,不然报异常

AtomicReferenceFieldUpdater AtomicIntegerFieldUpdater  AtomicLongFieldUpdater

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class TestFieldAtomic {
    private volatile int field;
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater =
                AtomicIntegerFieldUpdater.newUpdater(TestFieldAtomic.class, "field");
        TestFieldAtomic testFieldAtomic = new TestFieldAtomic();
        fieldUpdater.compareAndSet(testFieldAtomic, 0, 10);
        // 修改成功 field = 10
        System.out.println(testFieldAtomic.field);
        // 修改成功 field = 20
        fieldUpdater.compareAndSet(testFieldAtomic, 10, 20);
        System.out.println(testFieldAtomic.field);
        // 修改失败 field = 20
        fieldUpdater.compareAndSet(testFieldAtomic, 10, 30);
        System.out.println(testFieldAtomic.field);
    }
}


原子累加器

原子累加器有两类。其中LongAdder这一类的性能比AtomicLong要高。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class TestAccumulator {
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();
        // 4 个线程,每个累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start)/1000_000);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            demo(() -> new LongAdder(), adder -> adder.increment());
        }
        for (int i = 0; i < 5; i++) {
            demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
        }
    }
}

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。另外在设置累加单元时需要注意缓存行伪共享的问题。

    // 累加单元数组, 懒惰初始化
    // transient 表示这个域不会被序列化
    // 在并发编程中,序列化包含当前运行状态的字段可能会导致问题,比如反序列化后状态与预期不符。
    // 例如,cells数组中的Cell对象保存着临时的累加值,这些值在不同的时间点可能完全不同。将这种类型的数据序列化并在其他时间或环境中恢复,很可能会失去其初衷和准确性。
    transient volatile Cell[] cells;
    // 基础值, 如果没有竞争, 则用 cas 累加这个域
    transient volatile long base;
    // 在 cells 创建或扩容时, 置为 1, 表示加锁
    transient volatile int cellsBusy;

防止缓存行伪共享

    // 该注解是为了防止缓存行的伪共享
    @sun.misc.Contended
    static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }

        // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
        final boolean cas(long prev, long next) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
        }
        // 省略不重要代码
    }

缓存行

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte 8 long
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

伪共享

如果多个CPU读取的是同一个缓存行,那么一个CPU更新了这里面的某个值(并同步到内存),另外一个CPU还得从内存中读取。这种现象就是伪共享。如下图:

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
Core-0 要修改 Cell[0]  Core-1 要修改 Cell[1]
无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

加入了@sun.misc.Contended就能防止这种现象

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

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

相关文章

SAP代理商哲讯智能携手合肥企业,共塑SAP系统开发新篇章

在数字化转型的浪潮中&#xff0c;SAP开发作为企业信息化建设的重要一环&#xff0c;对于提升企业的运营效率和市场竞争力具有至关重要的作用。合肥&#xff0c;作为中国东部地区的重要城市之一&#xff0c;拥有众多企业正积极探索SAP开发的可能性。哲讯智能科技有限公司&#…

【昇思25天学习打卡营第25天 | 基于MindSpore通过GPT实现情感分类】

学习心得&#xff1a;基于MindSpore通过GPT实现情感分类 摘要 本文通过一个具体的实验案例&#xff0c;详细阐述了如何使用华为的MindSpore框架结合GPT模型来实现情感分类任务。从环境配置到模型训练&#xff0c;再到评估和测试&#xff0c;整个流程清晰、系统&#xff0c;为…

第三篇 Vue项目目录结构介绍

1、最外层目录结构 passagerFrontPage ├── .vscode //vscode配置&#xff0c;不用理会 ├── node_modules //项目依赖&#xff0c;npm install命令执行后自动生成 ├── public //公共资源存放 ├── src //源码 ├── tests //选装&#xff1a;测试模块 ├── .git…

华为云安全事件深度剖析与防范策略

华为云安全事件深度剖析与防范策略 引言 随着云计算技术的飞速发展&#xff0c;企业越来越依赖云服务来提升业务效率和创新能力。然而&#xff0c;云服务在带来便利的同时&#xff0c;也伴随着一系列复杂的安全挑战。华为云作为国内领先的云服务提供商&#xff0c;其安全性直接…

Linux网络——TcpServer

一、UDP 与 TCP 在现实生活中&#xff0c;Udp 类似于发传单&#xff0c;Tcp 类似于邮局的挂号信服务。 1.1 UDP&#xff08;用户数据报协议&#xff09; 无连接&#xff1a;发放传单时&#xff0c;你不需要提前和接受传单的人建立联系&#xff0c;直接把传单发出去。不可靠&…

Ubantu 使用 docker 配置 + 远程部署 + 远程开发

大家好我是苏麟 , Ubantu 一些配置 . 视频 : 服务器很贵&#xff1f;搞台虚拟机玩玩&#xff01;保姆级 Linux 远程开发教程_哔哩哔哩_bilibili Docker安装及配置 安装命令 : sudo apt install docker.io 查看版本号 : docker -v 查看虚拟机地址命令 : ifconfig 虚拟机地址 或…

服务器数据恢复—RAID5阵列重建重建导致数据丢失的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台服务器&#xff0c;有一组由5块硬盘组建的raid5磁盘阵列。 服务器在运行过程中一块有磁盘掉线&#xff0c;由于raid5阵列支持一块磁盘掉线的特性&#xff0c;服务器还在正常工作。不久之后服务器出现故障&#xff0c;管理员在不了…

记一下blender的烘焙贴图的UV特殊用法

就拿这个BOX来说好了 如果使用light map展开是这样的 如果你手动展好的话是这样的 为什么提到这个东西呢 如果有一个物体&#xff0c;你在blender渲染其实使用了程序化方案&#xff0c;且没展UV就做好了颜色&#xff0c;那如果你想要直接拿到他&#xff0c;直接用这个light …

LLM大模型实战项目--基于Stable Diffusion的电商平台虚拟试衣

本文详细讲解LLM大模型实战项目&#xff0c;基于Stable Diffusion的电商平台虚拟试衣 一、项目介绍 二、阿里PAI平台介绍 三、阿里云注册及开通PAI 四、PAI_DSW环境搭建 五、SDLORA模型微调 一、项目介绍 AI虚拟试衣是一种创新的技术&#xff0c;利用人工智能和计算机视觉技…

【C语言】深入解析选择排序

文章目录 什么是选择排序&#xff1f;选择排序的基本实现代码解释选择排序的优化选择排序的性能分析选择排序的实际应用结论 在C语言编程中&#xff0c;选择排序是一种简单且直观的排序算法。尽管它在处理大型数据集时效率不高&#xff0c;但由于其实现简单&#xff0c;常常用于…

IT产品研发全生命周期【详细说明】

阶段步骤任务负责人产品管理用户故事收集和理解用户需求&#xff0c;创建用户故事产品经理需求分类分类用户故事&#xff0c;组织和优先级排序需求经理可行性分析评估需求的技术可行性与实现难度研发经理需求转换将需求转化为具体的产品特性或功能要求需求经理需求管理创建需求…

【机器学习】机器学习与语音识别的融合应用与性能优化新探索

文章目录 引言第一章&#xff1a;机器学习在语音识别中的应用1.1 数据预处理1.1.1 数据去噪1.1.2 数据归一化1.1.3 特征提取 1.2 模型选择1.2.1 隐马尔可夫模型1.2.2 循环神经网络1.2.3 长短期记忆网络1.2.4 Transformer 1.3 模型训练1.3.1 梯度下降1.3.2 随机梯度下降1.3.3 Ad…

Three.JS 使用RGBELoader和CubeTextureLoader 添加环境贴图

导入RGBELoader模块&#xff1a; import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; 使用 addRGBEMappingk(environment, background,url) {rgbeLoader new RGBELoader();rgbeLoader.loadAsync(url).then((texture) > {//贴图模式 经纬…

基于Linux的USB-wifi配置流程

目录 内核配置 配置 CFG80211 配置usb 配置 Netlink 配置DHCP 工作流程 1.连接到无线网络 2.设置网络接口&#xff1a; 3.验证连接&#xff1a; 4. 接收数据&#xff1a; 最近daisy一直忙活这个linux的wifi驱动和bluze蓝牙驱动&#xff0c;相比较蓝牙&#xff0c;WiFi的驱动和内…

【ARMv8/v9 GIC- 700 系列 2 -- GIC-700 上电控制寄存器 GICR_PWRR】

请阅读【ARM GICv3/v4 实战学习 】 文章目录 GIC-700 上电GICR_PWRR 寄存器字段介绍GICR_PWRR 功能说明GICR_PWER 代码配置GICR_PWRR 使用场景GICR_PWRR 注意事项GIC-700 上电 GICR_PWRR(功耗寄存器)是ARM GICv4架构中用于控制GIC-700是否可以关闭电源的寄存器。它通过几个位…

【日记】我倒是想穿可爱的 JK 小裙子,可惜我是哥布林……(704 字)

正文 中午给三盆植物换水&#xff0c;惊叹于文竹的根。长得之长&#xff0c;都能在花盆里盘几圈了。而且我好像有一段时间没换水了&#xff0c;花盆的水中和盆底有了些绿藻。虽然不知道好不好&#xff0c;但我还是清掉了&#xff0c;摸起来黏黏的。而且我也总是觉得单位的水&am…

InceptionV3代码实现(Pytorch)

文章目录 Inception介绍InceptionV3代码实现第一步&#xff1a;定义基础卷积模块第二步&#xff1a;定义Inceptionv3模块InceptionAInceptionBInceptionCInceptionDInceptionE 第三步&#xff1a;定义辅助分类器InceptionAux第四步&#xff1a;搭建GoogLeNet网络第五步*&#x…

算法力扣刷题记录 五十二【617.合并二叉树】

前言 二叉树篇&#xff0c;继续。 记录 五十二【617.合并二叉树】 一、题目阅读 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中一棵覆盖到另一棵之上时&#xff0c;两棵树上的一些节点将会重叠&#xff08;而另一些不会&#xff09;。你需要…

HCIE-AI大模型直通车火热报名中

第一阶段&#xff1a;HCIA-AI Solution Architect&#xff08;直播&#xff0c;39课时&#xff09; 该阶段详细介绍 AI 大模型所需基础技术栈&#xff0c;包含深度学习基础、计算机视觉技术、自然语言处理技术、华为开源深度学习框架 MindSpore、注意力制、Transformer 架构&am…

【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【22】【RabbitMQ】

持续学习&持续更新中… 守破离 【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【22】【RabbitMQ】 Message Queue 消息队列异步处理应用解耦流量控制 消息中间件概念RabbitMQ概念MessagePublisherExchangeQueueBindingConnectionChannelConsumerVirtual HostBroker图…