从线程间通信聊到阻塞队列

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

很多Java新手都对ReentrantLock、阻塞队列以及等待唤醒机制十分感兴趣,于是一头扎进源码,想要吃透个中细节,却最终迷失方向什么都没学到。其实,从某个角度而言,ReentrantLock、阻塞队列、等待唤醒、AQS都是同一个东西,或者说它们被用来解决同一个问题:线程间通信。所以,本文打算从线程间通信讲起,由浅入深地介绍阻塞队列。后面如果有时间,还可以聊聊AQS及ReentrantLock。

ThreadPoolExecutor = 线程池 + 阻塞队列,本文是为了下一篇线程池做铺垫。

什么是线程间通信

定义:

针对同一个资源的操作有不同种类的线程。

说人话就是:共享资源+多线程,最典型的例子就是锁和生产者消费者(关于锁,后面有专门的章节介绍,这里以生产者-消费者为例子讲解)。

以现实生活为例。消费者和生产者就像两个线程,原本做着各自的事情,厂家管自己生产,消费者管自己买,一般情况下彼此互不影响。

但当物资到达某个临界点时,就需要根据供需关系适当作出调整。

  • 当厂家做了一大堆东西,产能过剩时,应该暂停生产,扩大宣传,让消费者过来消费

  • 当消费者发现某个热销商品售罄,应该提醒厂家尽快生产

在上面的案例中,生产者和消费者是不同种类的线程,一个负责存入,另一个负责取出,且它们操作的是同一个资源。但最难的部分在于:

  • 资源到达上限时,生产者等待,消费者消费
  • 资源达到下限时,生产者生产,消费者等待

你会发现,原本互不打扰的两个线程之间开始“沟通”了:

  • 生产者:喂,我这边做的太多了,先休息会儿,你赶紧消费
  • 消费者:喂,货快没了,我休息会儿,你赶紧生产

这种线程间的相互调度,也就是线程间通信。

看到这,你心里暗暗想道:我擦,我只会new Thread().start(),怎么让A线程去喊B线程工作呢?

实现线程间通信

还是以上面的生产者-消费者为例,有很多种方式可以实现线程间通信。

轮询

设计理念:生产者和消费者线程各自使用while循环,每隔片刻就去判断Queue的状态,队列为空时生产者才可插入数据,队列不为空时消费者才能取出数据,否则一律sleep等待。

/**
 * 轮询版本
 */
public class WhileQueue<T> {
    // 容器,用来装东西
    private final LinkedList<T> queue = new LinkedList<>();

    public void put(T resource) throws InterruptedException {
        while (queue.size() >= 1) {
            // 队列满了,不能再塞东西了,轮询等待消费者取出数据
            System.out.println("生产者:队列已满,无法插入...");
            TimeUnit.MILLISECONDS.sleep(1000);
        }
        System.out.println("生产者:插入" + resource + "!!!");
        queue.addFirst(resource);
    }

    public void take() throws InterruptedException {
        while (queue.size() <= 0) {
            // 队列空了,不能再取东西,轮询等待生产者插入数据
            System.out.println("消费者:队列为空,无法取出...");
            TimeUnit.MILLISECONDS.sleep(1000);
        }
        System.out.println("消费者:取出消息!!!");
        queue.removeLast();
        TimeUnit.MILLISECONDS.sleep(5000);
    }

}

测试:

public class Test {
    public static void main(String[] args) {
        // 队列
        WhileQueue<String> queue = new WhileQueue<>();

        // 生产者
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    try {
                        queue.put("消息" + i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        // 消费者
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    try {
                        queue.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

由于设定了队列最多只能存1个消息,所以只有当队列为空时,生产者才能插入数据。这是最简单的线程间通信:

多个线程不断轮询共享资源,通过共享资源的状态判断自己下一步该做什么。

看到这,你发现自己被骗了:哦,原来要实现线程间通信,并非真的需要A线程直接去叫B线程干什么,不同线程通过共享变量即可完成通信!

但上面的实现方式存在一些缺点:

  • 轮询的方式太耗费CPU资源,如果线程过多,比如几百上千个线程同时在那轮询,会给CPU带来较大负担
  • 无法保证原子性(代码里没有演示,但理论上确实如此,如果生产者的操作非原子性,消费者极可能获取到脏数据)

等待唤醒机制:wait/notify

相对而言,等待唤醒机制则要优雅得多,底层通过维护线程队列的方式,避免了过多线程同时自旋造成的CPU资源浪费,颇有点“用空间换时间”的味道。当一个生产者线程无法插入数据时,就让它在队列里休眠(阻塞),此时生产者线程会释放CPU资源,等到消费者抢到CPU执行权并取出数据后,再由消费者唤醒生产者继续生产。

举个例子,原本生产者和消费者都要时不时去店里看一下:

  • 生产者:货卖完了没有,卖完了我要继续生产(每分钟来店里看一下)
  • 消费者:补货了没,补货了我就可以买了(每分钟来店里看一下)

而现在,生产者去店里看了下,发现还有货,就管自己去后厨睡觉了,等店里货都卖完了,自然会有消费者过来喊他补货,不需要付出额外的精力在店里盯着。

Java有多种方式可以实现等待唤醒机制,最经典的就是wait和notify。

/**
 * wait/notify版本
 */
public class WaitNotifyQueue<T> {
    // 容器,用来装东西
    private final LinkedList<T> queue = new LinkedList<>();

    public synchronized void put(T resource) throws InterruptedException {
        while (queue.size() >= 1) {
            // 队列满了,不能再塞东西了,轮询等待消费者取出数据
            System.out.println("生产者:队列已满,无法插入...");
            this.wait();
        }
        System.out.println("生产者:插入" + resource + "!!!");
        queue.addFirst(resource);
        this.notify();
    }

    public synchronized void take() throws InterruptedException {
        while (queue.size() <= 0) {
            // 队列空了,不能再取东西,轮询等待生产者插入数据
            System.out.println("消费者:队列为空,无法取出...");
            this.wait();
        }
        System.out.println("消费者:取出消息!!!");
        queue.removeLast();
        this.notify();
    }
}

对比WhileQueue做了哪些改进:

  • 用synchronized保证原子性
  • wait和notify实现等待唤醒

但一般推荐使用notifyAll(为什么?)。我们给测试程序再加一个生产者线程就知道了:

我们发现,整个程序所有线程都阻塞了。

原因是:在synchronized机制下,所有等待的线程都在同一个队列里(生产者和消费者共用一个队列),而notify又恰巧是随机唤醒线程。也就是说,有可能当前醒着的唯一线程是生产者,而他干完活以后唤醒的又是生产者。

最终结果是:所有线程都睡觉了...表现在程序上,就是卡住了。

解决办法是改用notifyAll,把所有线程都唤醒,然后大家一起参与执行权的竞争。你是否有疑问:如果和上面一样,生产者1还是唤醒生产者2呢?

其实这个假设不成立...使用notifyAll以后就不再是随机唤醒某一个线程了,而是唤醒所有线程并重新抢夺执行权。也就是说,每一个线程在进入阻塞之前,都会叫醒其他所有线程!

等待唤醒机制:condition

wait/notify版本的缺点是随机唤醒容易出现“己方唤醒己方,最终导致全部线程阻塞”的乌龙事件,虽然wait/notifyAll能解决这个问题,但唤醒全部线程又不够精确,会造成无谓的线程竞争(实际只需要唤醒敌方线程即可)。

作为改进版,可以使用ReentrantLock的Condition替代synchronized的wait/notify:

/**
 * Condition版本
 */
public class ConditionQueue<T> {
    // 容器,用来装东西
    private final LinkedList<T> queue = new LinkedList<>();

    // 显式锁(相对地,synchronized锁被称为隐式锁)
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition producerCondition = lock.newCondition();
    private final Condition consumerCondition = lock.newCondition();

    public void put(T resource) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() >= 1) {
                // 队列满了,不能再塞东西了,轮询等待消费者取出数据
                System.out.println("生产者:队列已满,无法插入...");
                // 生产者阻塞
                producerCondition.await();
            }
            System.out.println("生产者:插入" + resource + "!!!");
            queue.addFirst(resource);
            // 生产完毕,唤醒消费者
            consumerCondition.signal();
        } finally {
            lock.unlock();
        }
    }

    public void take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() <= 0) {
                // 队列空了,不能再取东西,轮询等待生产者插入数据
                System.out.println("消费者:队列为空,无法取出...");
                // 消费者阻塞
                consumerCondition.await();
            }
            System.out.println("消费者:取出消息!!!");
            queue.removeLast();
            // 消费完毕,唤醒生产者
            producerCondition.signal();
        } finally {
            lock.unlock();
        }
    }
}

如何理解Condition呢?你可以认为lock.newCondition()创建了一个队列,调用producerCondition.await()会把生产者线程放入生产者的等待队列中,当消费者调用producerCondition.signal()时会唤醒从生产者的等待队列中唤醒一个生产者线程出来工作。

后续condition.signal()就是从等待队列唤醒线程

也就是说,ReentrantLock的每个Condition都会创建一个等待队列的方式,可以分别存储需要等待的生产者线程和消费者线程,从而实现“精准唤醒”。

山寨版BlockingQueue

至此,大家应该对线程间通信有了大致了解。如果你仔细观察,会发现上面其实都采用了阻塞队列实现。我们都是先构造一个Queue,然后生产者和消费者直接操作Queue,至于是否阻塞,由Queue内部判断。这样封装的好处是,将生产者和消费者解耦的同时,不暴露过多细节,使用起来更简单(让生产者、消费者自己去判断是否需要阻塞会很繁琐)。

大家应该都听过JDK的阻塞队列吧?基于上面的案例,我们改进一下,抽取出一个自定义的阻塞队列(使用wait/nofityAll实现):

public class BlockingQueue<T> {

    // 模拟队列
    private final LinkedList<T> queue = new LinkedList<>();

    private int MAX_SIZE = 1;
    private int remainCount = 0;

    public BlockingQueue(int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException("size最小为1");
        }
        this.MAX_SIZE = capacity;
    }

    public synchronized void put(T resource) throws InterruptedException {
        while (queue.size() >= MAX_SIZE) {
            // 队列满了,不能再塞东西了,阻塞生产者
            System.out.println("插入阻塞...");
            this.wait();
        }
        queue.addFirst(resource);
        remainCount++;
        printMsg(resource, "被插入");
        this.notifyAll();
    }

    public synchronized T take() throws InterruptedException {
        while (queue.size() <= 0) {
            // 队列空了,不能再取东西了,阻塞消费者
            System.out.println("取出阻塞...");
            this.wait();
        }
        T resource = queue.removeLast();
        remainCount--;
        printMsg(resource, "被取出");
        this.notifyAll();
        return resource;
    }

    private void printMsg(T resource, String operation) throws InterruptedException {
        System.out.println(resource + operation);
        System.out.println("队列容量:" + remainCount);
    }
}

BlockingQueue简介

虽然很多人开口闭口“阻塞队列”,但“阻塞队列”在他脑中只是个很模糊的概念。连“阻塞队列”的来龙去脉都不甚清楚,又怎么能说了解呢?

实际上,和List、Set一样,“阻塞队列”也有自己的一脉。在JDK的util包下有一个Queue接口:

如果你继续往下扒,就会发现Queue和List其实很像,也是集合的一个分支罢了:

为什么很多人会觉得阻塞队列(比如ArrayBlockingQueue)高大上,听起来比ArrayList牛逼呢?主要在于“阻塞”二字!因为大家不了解阻塞,自己也不知道怎么实现阻塞,所以会觉得阻塞队列很神秘,很牛逼。但仔细观察上面的继承关系你会发现,如果ArrayBlockingQueue没有实现BlockingQueue接口,那么它本应该是个普普通通的队列,而不是阻塞队列,也就没有那么惊艳了。

那么BlockingQueue做了啥呢?其实啥也没做,毕竟BlockingQueue只是个接口,而接口只能定义方法...就好比一栋摩天大厦建成了,楼顶有个空中泳池,你觉得很牛逼。那么,你觉得是当初说“我要楼顶有个大花园”的老板牛逼还是把这个方案实现的设计师牛逼呢?

扯远了,其实BlockingQueue继承Queue接口后,就定义了几个方法:

失败抛异常

失败返回特殊值

阻塞

阻塞(指定超时时间)

插入

add(e)

offer(e)

put(e)

offer(e, time, unit)

删除

remove()

poll()

take()

poll(time, unit)

查询

element()

peek()

BlockingQueue金口一开,后面的小弟只能满足,所以几个阻塞队列的实现类都有上面的几个方法。

那么阻塞队列的“阻塞”是怎么实现的呢?以ArrayBlockingQueue为例,通过上面的继承关系分析,Queue和BlockingQueue是接口,里面只有方法定义没有具体实现,有可能实现“阻塞”功能的要么在AbstractQueue,要么就是ArrayBlockingQueue自身。我们查看AbstractQueue发现这家伙几乎啥都没写...

也就是说,当初老板发话“我希望这个队列能阻塞”,经理微笑着满口答应,结果转手就交给3个小弟自己整了。好在3个小弟争气,还真给他们搞出来了...

常用的3个阻塞队列:

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchronousQueue

还是以ArrayBlockingQueue为例,它是怎么实现阻塞的呢?put()方法内部判断,队列满了就notFull.await(),否则enqueue()。也就是说,队列满了就阻塞,没满就入队。

好家伙...竟然用了ReentrantLock,这和我们上面案例中写的ConditionQueue好像啊!

但是ArrayBlockingQueue只有notFull.await(),没看到signal(),不合理。仔细找找,唯一的可能是ArrayBlockingQueue把signal()藏在enqueue(e)方法里了:

也就是说,生产者每次成功插入都尝试唤醒消费者组,让它们来消费。虽然极端情况下可能被唤醒的消费者仍然抢不到锁,但没关系,put()内部会判断队列是否满了,一旦队列满了,无论来多少个生产者,统统给你阻塞住,极端情况下生产者全部阻塞,总会轮到消费者的。消费者则是每次成功消费都尝试唤醒生产者组。

其他两个阻塞队列LinkedBlockingQueue和SynchronousQueue同理,也是用ReentrantLock实现阻塞的。

展望AQS

看到这里,相信阻塞队列在大家心中已经不再那么神圣了,有什么了不起啊,我们自己也能写啊,还用了好几种方式实现呢!但是扪心自问,阻塞队列总共也就:

  • 阻塞
  • 队列

而我们所谓的手写阻塞队列,其实是这样的:队列直接用了LinkedList,阻塞也是借用wait/notify和ReentrantLock实现的。也就是说,我们其实只是做了组装工作,拿现成的队列+阻塞功能拼出了一个阻塞队列

世间路千万条,总有人不走寻常路。按理说现成的List+wait/notifyAll已经可以造出阻塞队列了,但就是有大佬不满足。

Doug Lea老爷子震惊的说:What?! Why you don't say earlly ya! I have already finished the AQS le...

是的,又是这个男人,他整出了一个AQS,再把AQS塞到ReentrantLock中,最后用ReentrantLock+数组、ReentrantLock+链表、ReentrantLock+Transfer搞出了ArrayBlockingQueue、LinkedBlockingQueue和SynchronousQueue...阻塞队列只能算顺便的,他的初衷其实是利用AQS统一并简化锁的实现,屏蔽同步状态管理、阻塞线程的排队和通知、唤醒机制等,让后续的二次开发更简便。

换句话说:

如果你纠结于阻塞队列怎么实现,那你的格局就太小了...JDK的阻塞队列依赖于ReentrantLock,而ReentrantLock只是对AQS的浅封装,真正需要我们花功夫学习的其实有且只有AQS。

学完本篇,我鼓励你现在、马上、立刻打开IDEA搜索ArrayBlockingQueue,把源码看一遍。相信我,你已经可以驯服它!

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#科学绘图库ScottPlot

文章目录 安装和准备初步使用简单的设置 安装和准备 ScottPlot是基于.Net的一款开源免费的交互式可视化库&#xff0c;支持Winform和WPF等UI框架&#xff0c;本文示例在WPF环境中运行。在VS的菜单栏->工具->NuGet包管理器->管理解决方案的NuGet程序包->在浏览选项…

WordCount 源码解析 Mapper,Reducer,Driver

创建包 com.nefu.mapreduce.wordcount &#xff0c;开始编写 Mapper &#xff0c; Reducer &#xff0c; Driver 用户编写的程序分成三个部分&#xff1a; Mapper 、 Reducer 和 Driver 。 &#xff08; 1 &#xff09; Mapper 阶段 ➢ 用户自定义的 Mapper 要继承自己的父…

电话卡Giffgaff激活

Giffgaff是一家总部位于英国的移动电话公司。作为一家移动虚拟网络电信运营商&#xff0c;Giffgaff使用O2的网络&#xff0c;是O2的全资子公司&#xff0c;成立于2009年11月25日。 Giffgaff与传统的移动电话运营商不同&#xff0c;区别在于其用户也可以参与公司的部分运营&…

lazada来赞达API开发系列:item_get - 获得lazada商品详情API返回值说明

Lazada API接口的作用主要体现在以下几个方面&#xff1a; 获取商品信息&#xff1a;通过Lazada API接口&#xff0c;开发者可以获取Lazada平台上的商品详细信息&#xff0c;包括商品的名称、价格、图片、描述、规格、库存等&#xff0c;这些信息有助于用户了解商品特点、性能…

Xshell应用程序无法正常启动0xc000007b

重启&#xff0c;卸载重装&#xff0c;添加csdn教程里的缺失文件没用下载修复工工具也没弄出来之后&#xff0c;在他的工具下拉栏里调成增强模式增加C&#xff0c;功能成功解决。如下图在重新修复&#xff0c;他中途会有捐赠提示&#xff0c;可以关闭成功后再支持&#xff0c;我…

MES系统中的生产调度流程你了解多少?

万界星空科技专注于制造业生产&#xff08;MES&#xff09;管理平台的研发和实施&#xff0c;已成功帮助很多企业和工厂解决了内部的管理问题&#xff0c;有效的提高了生产效率&#xff0c;并且节省了人力。成功应用于汽车、高科技电子、注塑、电线电缆、造鞋、设备制造、新能源…

Vue.js实现可编辑表格并高亮修改的单元格

实现一个可编辑的表格&#xff0c;让用户可以修改表格中的数据&#xff0c;并且能够清楚地看到哪些单元格被修改过。这样的功能可以提高用户体验&#xff0c;也方便后端处理数据的变化。 本文将介绍如何使用Vue.js和Element UI的el-table组件来实现一个可编辑表格&#xff0c;…

postcss-pxtorem实现页面自适应的原理

先声明一点这玩意本身不能实现哈&#xff0c;他只是一个工具&#xff0c;更是一个postcss的插件 帮助我们从px转化成为rem比如我们的代码 div {height: 100px;width: 100px; }经过这个插件转化之后变成 假设变成下面这样哈 div {height: 1rem;width: 1rem; }其他没啥子太大作…

GateWay网关介绍以及整合knife4j聚合所有服务的接口文档

为什么使用网关&#xff1f; 因为多个微服务的端口不同&#xff0c;前端调用不方便&#xff0c;使用网关可以统一接收处理前端的请求&#xff0c;同时方便接口的集中处理&#xff0c;比如鉴权、聚合接口文档、限流等等.. 这里使用Knife4j文档工具来实现接口文档&#xff1a;K…

【分布式微服务专题】从单体到分布式(二、SpringCloud整合Nacos)

目录 前言阅读对象阅读导航前置知识笔记正文一、下载安装二、项目整合2.1 服务注册与发现2.2 动态配置管理 三、其他实验四、服务之间的调用 学习总结感谢 前言 本篇笔记主要是记录我整合Nacos项目进来的过程。以实现服务注册发现&#xff0c;以及分布式配置管理。关于Nacos&a…

hive数据库查看参数/hive查看当前环境配置

文章目录 一、hive查看当前环境配置命令 在一次hive数据库执行命令 set ngmr.exec.modecluster时&#xff0c;想看一下 ngmr.exec.mode参数原先的值是什么&#xff0c;所以写一下本篇博文&#xff0c;讲一下怎么查看hive中的参数。 一、hive查看当前环境配置命令 set &#…

06 硬件知识入门(MOSS管)

1 简介 MOS管和三极管的驱动方式完全不一样&#xff0c;以NPN型三极管为例&#xff0c;base极以小电流打开三极管&#xff0c;此时三极管的集电极被打开&#xff0c;发射极的高电压会导入&#xff0c;此时电流&#xff1a;Ic IbIe &#xff1b;电压&#xff1a;Ue>Uc>Ub…

DBSCAN算法原理及其Python实现

文章目录 原理Python实现验证 原理 DBSCAN&#xff0c;即Density-Based Spatial Clustering of Applications with Noise&#xff0c;基于密度的噪声应用空间聚类。 在DBSCAN算法中&#xff0c;将数据点分为三类&#xff1a; 核心点&#xff1a;若样本 x i x_i xi​的 ε \v…

《深入理解计算机系统》学习笔记 - 第三课 - 浮点数

Floating Point 浮点数 文章目录 Floating Point 浮点数分数二进制示例能代表的数浮点数的表示方式浮点数编码规格化值规格化值编码示例 非规格化的值特殊值 示例IEEE 编码的一些特殊属性四舍五入&#xff0c;相加&#xff0c;相乘四舍五入四舍五入的模式二进制数的四舍五入 浮…

Linux 网络协议

1 网络基础 1.1 网络概念 网络是一组计算机或者网络设备通过有形的线缆或者无形的媒介如无线&#xff0c;连接起来&#xff0c;按照一定的规则&#xff0c;进行通讯的集合( 缺一不可 )。 5G的来临以及IPv6的不断普及&#xff0c;能够进行联网的设备将会是越来越多&#xff08…

扩展学习|商务智能与社会计算

一、概念介绍 &#xff08;一&#xff09;商务智能 商务智能&#xff08;Business Intelligence&#xff0c;简称BI&#xff09;是一种基于数据分析的决策支持系统&#xff0c;旨在帮助企业或组织更好地理解和利用自身数据&#xff0c;发现其中的模式和趋势&#xff0c;并提供…

Java物联网项目源码

Java物联网项目源码 使用技术&#xff1a;JAVA [ springmvc / spring / mybatis ] 、Mysql 、Html 、Jquery 、css 协议和优势&#xff1a;TCP/IP、HTTP、MQTT 通讯协议。 系统包括&#xff1a;后台服务&#xff0c;传感器解析服务、web展示&#xff1b; 目前web系统支持功…

【Proteus仿真】【STM32单片机】蓝牙遥控小车

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真STM32单片机控制器&#xff0c;使LCD1602液晶&#xff0c;L298电机&#xff0c;直流电机&#xff0c;HC05/06蓝牙模块等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显…

基于springboot实现的仿天猫商城项目

一、系统架构 前端&#xff1a;jsp | js | css | jquery 后端&#xff1a;springboot | mybatis-plus 环境&#xff1a;jdk1.7 | mysql | maven 二、代码及数据库 三、功能介绍 01. web端-首页 02. web端-商品查询 03. web端-商品详情 04. web端-购物车 05. web端-订单…

YOLOv8改进 | 2023 | DiverseBranchBlock多元分支模块(有效涨点)

一、本文介绍 本文带来的改进机制是YOLOv8模型与多元分支模块&#xff08;Diverse Branch Block&#xff09;的结合&#xff0c;Diverse Branch Block (DBB) 是一种用于增强卷积神经网络性能的结构重新参数化技术。这种技术的核心在于结合多样化的分支&#xff0c;这些分支具有…