AQS是什么?AbstractQueuedSynchronizer之AQS原理及源码深度分析
文章目录
- 一、AQS概述
- 1、什么是AQS
- 2、技术解释
- 3、基本原理
- 4、AQS为什么这么重要
- 二、AQS数据结构
- 1、AQS的结构
- 2、ReentrantLock与AbstractQueuedSynchronizer
- 3、AQS的state变量
- 4、AQS的队列
- 5、AQS的Node
- (1)Node的waitStatus
- (2)属性说明
- 三、ReentrantLock的lock方法分析AQS源码
- 1、类图
- 2、FairSync和NonfairSync
- 3、tryAcquire():尝试获取锁
- 4、addWaiter():入队
- 5、acquireQueued():队列线程挂起
- 6、cancelAcquire():中断等异常导致的取消
- 四、ReentrantLock的unlock方法分析AQS源码
- 1、unlock
- 2、tryRelease():释放锁
- 3、unparkSuccessor():唤醒等待线程
- 五、大总结
- 参考资料
一、AQS概述
1、什么是AQS
AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。
其中,AbstractOwnableSynchronizer是AbstractQueuedLongSynchronizer和AbstractQueuedSynchronizer的父类。
其中AbstractOwnableSynchronizer和AbstractQueuedLongSynchronizer是jdk1.6引入的,AbstractQueuedSynchronizer是jdk1.5引入的。
2、技术解释
AQS是用来实现锁或者其他同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给“谁”的问题。
AQS用于实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件等)。并通过一个int类型变量表示持有锁的状态。
如图所示,AQS利用CLH(Craig、Landin and Hagersten,是一个单向链表,但AQS中的队列是CLH变体的虚拟双向队列FIFO)队列 加上一个int类型的公共变量实现的。
AbstractQueuedLongSynchronizer:此类具有与完全相同 AbstractQueuedSynchronizer 的结构、属性和方法,不同之处在于所有与状态相关的参数和结果都定义为 long 而不是 int。在创建同步器(如需要 64 位状态的多级锁和屏障)时,此类可能很有用。AbstractQueuedLongSynchronizer并没有具体实现类,目前还是用的AbstractQueuedSynchronizer。
3、基本原理
抢到资源的线程直接使用处理业务逻辑,抢不到资源的线程必然需要排队等候
,而存储这些排队等候的资源就是AQS中的队列(CLH队列的变体-双端队列)。
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要是CLH队列的变体来实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象实现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自选以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
4、AQS为什么这么重要
下图是AQS的众多实现类,我们发现,JUC中的许多非常重要的类,都有着AQS的影子:
我们平常用的ReentrantLock、Semaphore、CountDownLatch等等,这都是定义了程序员和锁交互的使用层API,隐藏了实现细节,使用就可以。而AQS就是统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等等。所以,AQS是一切锁和同步组件实现的公共基础部分
。
二、AQS数据结构
1、AQS的结构
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,通过CAS完成对state值的修改。
其中Node中也包含头指针和尾指针,也包含了一个重要的属性waitStatus,这里区别于AQS的state。
2、ReentrantLock与AbstractQueuedSynchronizer
3、AQS的state变量
该变量位0时意味着没有线程占用,大于等于1意味着有线程占用,其它线程需要等候。
4、AQS的队列
CLH:Craig、Landin and Hagersten,是一个单向链表,但AQS中的队列是CLH变体的虚拟双向队列FIFO。
其数据结构图如下:
官方图是单向链表,但是AQS实质是双端队列。
5、AQS的Node
Node分共享和独占模式
。
(1)Node的waitStatus
Node就是等候区的线程的存储单元,一个线程对应一个Node。
Node的等待状态waitStatus是一个int类型的变量(区别于AQS的stete)。总共有如下几种:
// 线程被取消了
static final int CANCELLED = 1;
// 后继线程需要唤醒
static final int SIGNAL = -1;
// 等待condition唤醒
static final int CONDITION = -2;
// 共享或同步状态获取将会无条件地传播下去
static final int PROPAGATE = -3;// 初始为0,状态是上面几种
volatile int waitStatus;// 前驱结点
volatile Node prev;// 后继节点
volatile Node next;// 当前线程
volatile Thread thread;
(2)属性说明
其中waitStatus共有5个状态:
1.为0时,当第一个Node被初始化时的默认值。
2.为CANCELLED时(-1),表示线程获取锁的请求已经取消了。
3.为CONDITION(-2),表示节点在等待队列中,节点线程等待唤醒。
4.为PROPAGATE(-3),当前线程处在SHARED情况下,该字段才会使用。
5.SIGNAL(-1),表示线程已经准备好了,就等资源释放了。
三、ReentrantLock的lock方法分析AQS源码
1、类图
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类(Sync)完成线程访问控制的。
2、FairSync和NonfairSync
FairSync是公平锁,讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
NonfairSync是非公平锁,不管是否有等待队列,如果可以获取锁,则立刻占有锁的对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获取锁,它还是需要参加竞争(存在线程竞争的情况下),所以有可能出现后来的线程插队获取锁的现象。
它们的源码相差很小:
最终调用的acquire(1),就是AQS的核心方法:
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
3、tryAcquire():尝试获取锁
tryAcquire是AQS中定义的,但是由FairSync和NonfairSync重写:
我们分析NonfairSync:tryAcquire方法。
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // 获取锁状态if (c == 0) { // 如果资源未被占用,compareAndSetState使用cas获取锁,设置锁状态if (compareAndSetState(0, acquires)) {// 设置所属线程为当前线程setExclusiveOwnerThread(current);// 获取锁return true;}}// 如果当前资源被占用,并且是当前线程(可重入)else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc); // state + 1,仍然可以获取锁(可重入)return true;}return false;
}
所以,tryAcquire()是相当于做了一次尝试,尝试获取锁,如果获取锁那就皆大欢喜,获取不到就继续addWaiter(Node.EXCLUSIVE), arg)方法。
4、addWaiter():入队
// java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
// mode – Node.EXCLUSIVE 表示独占,Node.SHARED 表示共享
private Node addWaiter(Node mode) {// 一个Node包含一个当前线程的实例Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail; // 尾指针 ,初始为nullif (pred != null) { // 不为null时node.prev = pred;if (compareAndSetTail(pred, node)) { // 设置尾指针为当前pred.next = node;return node;}}// 入队enq(node);return node;
}// 将节点插入队列,必要时进行初始化
private Node enq(final Node node) {for (;;) { // 死循环不断重试Node t = tail; // 尾结点if (t == null) { // Must initialize 虚拟头结点必须初始化,里面没有线程if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) { // 设置尾结点为当前节点t.next = node; // 所有的Node形成一条链表return t;}}}
}
最终形成如下的数据结构:
5、acquireQueued():队列线程挂起
// arg = 1
// Node是当前节点
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// predecessor获取前置节点final Node p = node.predecessor();// 如果是头结点,直接tryAcquire尝试获取锁if (p == head && tryAcquire(arg)) {// 获取锁成功,将node设为头结点setHead(node);// 将头结点的关联取消p.next = null; // help GCfailed = false;return interrupted;}// 尝试获取锁失败 或者不是第一个节点// shouldParkAfterFailedAcquire : 设置前置节点的waitStatue0->-1,首次false,再次为true,再来个线程直接trueif (shouldParkAfterFailedAcquire(p, node) &&// LockSupport.park暂停当前线程parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 获取前驱结点的状态int ws = pred.waitStatus; // 0if (ws == Node.SIGNAL) // Node.SIGNAL :-1 即等待被占用的资源释放,直接返回true/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) { // > 0是CANCELLED状态,忽略改状态的节点,重新链接队列/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else { // 将当前节点的前驱结点设置为SIGNAL : -1,用于后续唤醒操作/** waitStatus must be 0 or PROPAGATE. Indicate that we* need a signal, but don't park yet. Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 把前置节点的waitStatue 改为 -1}return false;
}
// 暂停
private final boolean parkAndCheckInterrupt() {LockSupport.park(this); // 线程挂起,等待被unpark、被中断return Thread.interrupted(); // 返回当前线程中断状态,并清空中断状态
}
6、cancelAcquire():中断等异常导致的取消
如果在自旋中,并没有获取锁,而是异常原因中断了,就会执行finally 中的cancelAcquire方法。
private void cancelAcquire(Node node) {// Ignore if node doesn't existif (node == null)return;node.thread = null;// Skip cancelled predecessorsNode pred = node.prev; // 前一个节点while (pred.waitStatus > 0) // CANCELLED:1,再找前一个节点node.prev = pred = pred.prev;// predNext is the apparent node to unsplice. CASes below will// fail if not, in which case, we lost race vs another cancel// or signal, so no further action is necessary.Node predNext = pred.next; // 下一个节点// Can use unconditional write instead of CAS here.// After this atomic step, other Nodes can skip past us.// Before, we are free of interference from other threads.node.waitStatus = Node.CANCELLED; // 本节点的waitStatue设置为CANCELLED// If we are the tail, remove ourselves.if (node == tail && compareAndSetTail(node, pred)) { // 如果本节点为尾结点,就将尾结点设为上一个不为CANCELLED的节点compareAndSetNext(pred, predNext, null); // 前一节点的next节点设为null} else { // 如果不是队尾// If successor needs signal, try to set pred's next-link// so it will get one. Otherwise wake it up to propagate.int ws;if (pred != head && // 上一个不为CANCELLED的节点不是head节点((ws = pred.waitStatus) == Node.SIGNAL || // 上一个不为CANCELLED的节点waitStatus 为SIGNAL -1(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { // 上一个不为CANCELLED的节点 thread不为nullNode next = node.next; // 本节点的nextif (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next); // 上一个不为CANCELLED的节点 的next 设为为本节点的next} else {unparkSuccessor(node); // 本节点unlock}node.next = node; // help GC}
}
四、ReentrantLock的unlock方法分析AQS源码
1、unlock
public void unlock() {sync.release(1);
}
public final boolean release(int arg) {if (tryRelease(arg)) { // 释放state为0Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h); // 唤醒return true;}return false;
}
2、tryRelease():释放锁
protected final boolean tryRelease(int releases) {int c = getState() - releases;// 1 -1 = 0if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true; // 设置占用线程为nullsetExclusiveOwnerThread(null);}setState(c); state设为0return free;
}
3、unparkSuccessor():唤醒等待线程
private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling. It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus; // 头结点的waitStatue ,有线程占用的话为-1if (ws < 0) // 设为0compareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node. But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*/Node s = node.next; // 头结点的下一个节点if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null) // 唤醒下一个线程LockSupport.unpark(s.thread);
}
五、大总结
(图片模糊的话,保存下来本地看)
参考资料
https://zhuanlan.zhihu.com/p/378219920
相关文章:

AQS是什么?AbstractQueuedSynchronizer之AQS原理及源码深度分析
文章目录 一、AQS概述1、什么是AQS2、技术解释3、基本原理4、AQS为什么这么重要 二、AQS数据结构1、AQS的结构2、ReentrantLock与AbstractQueuedSynchronizer3、AQS的state变量4、AQS的队列5、AQS的Node(1)Node的waitStatus(2)属性…...
【数据库】函数处理(文本处理函数、日期和时间处理函数、数值处理函数)
函数处理数据 算术运算函数文本处理函数日期和时间处理函数数值处理函数 算术运算 操作符说明加-减*乘/除 e . g . e.g. e.g. 列出 Orders 表中所有每项物品的 id,数量 quantity,单价 item_price,总价 expanded_price(数量 * 单价…...

GEE案例——一个完整的火灾监测案例dNBR差异化归一化烧毁指数
差异化归一化烧毁指数 dNBR是"差异化归一化烧毁指数"的缩写。它是一种用于评估卫星图像中烧毁区域严重程度的遥感指数。dNBR值通过将火灾前的归一化烧毁指数(NBR)减去火灾后的NBR来计算得出。该指数常用于野火监测和评估。 dNBR(差异化归一化烧毁指数)是一种用…...

计算机算法分析与设计(20)---回溯法(0-1背包问题)
文章目录 1. 题目描述2. 算法思路3. 例题分析4. 代码编写 1. 题目描述 对于给定的 n n n 个物品,第 i i i 个物品的重量为 W i W_i Wi,价值为 V i V_i Vi,对于一个最多能装重量 c c c 的背包,应该如何选择放入包中的物品…...
什么是IO多路复用?Redis中对于IO多路复用的应用?
IO多路复用是一种高效的IO处理方式,它允许一个进程同时监控多个文件描述符(包括套接字、管道等),并在有数据可读或可写时进行相应的处理。这种机制可以大大提高系统的并发处理能力,减少资源的占用和浪费。 在Redis中&…...

NanoPC-T4 RK3399:DTS之io-domain,FAN
前言: 之后所有改动均是基于rk3399-evb.dts修改以满足NanoPC-T4功能正常。 NanoPC-T4开发板上有一片散热风扇,本章将讲述使风扇正常工作起来的多种方法。 一:硬件分析 GPIO4_C6/PWM1:实际控制风扇引脚,GPIO与PWM复用 输入高电平1:FAN2pin电路导通,风扇转动 输入低电…...
vue3+vite+ts项目使用jQuery
1、安装jQuery npm install --save jquery 2、安装声明文件 npm install --save types/jquery 3、在需要的文件中引入 import $ from jquery...
一起学数据结构(10)——排序
从本文开始,通过若干篇文章展开对于数据结构中——排序的介绍。 1. 排序的概念: 将一堆杂乱无章的数据,通过一定的规律顺序排列起来。即将一个无序序列排列成一个有序序(由小到大或者由大到小)的运算。 在数据的排序中…...

php 数组基础/练习
数组 练习在最后 数组概述 概述与定义 数组中存储键值对 数组实际上是一个有序映射 key-value,可将其当成真正的数组、列表(向量)、散列表、字典、集合、栈、队列等 数组中的元素可以是任意类型的数据对象(可以嵌套数组&#…...

Redbook Chapter 7: Query Optimization翻译批注
首先说明一下redbook上的几篇文章是做什么的。这几篇文章是通过几位作者对不同方面的论文进行阅读和筛选后,挑出其中具备代表性或者权威的论文来做分析,为读者提供阅读指导和建议,同时,也是对某个方面的论文进行高度的总结&#x…...

【分布式】大模型分布式训练入门与实践 - 04
大模型分布式训练 数据并行-Distributed Data Parallel1.1 背景1.2 PyTorch DDP1) DDP训练流程2)DistributedSampler3)DataLoader: Parallelizing data loading4)Data-parallel(DP)5)DDP原理解析…...

欧拉图相关的生成与计数问题探究
最近学了一波国家集训队2018论文的最后一个专题。顺便带上了一些我的注解。 先放一波这个论文 1.基本概念 欧拉图问题是图论中的一类特殊的问题。在本文的介绍过程中,我们将会使用一些图 论术语。为了使本文叙述准确,本节将给出一些术语的定义。 定义…...

CSS3属性详解(一)文本 盒模型中的 box-ssize 属性 处理兼容性问题:私有前缀 边框 背景属性 渐变 前端开发入门笔记(七)
CSS3是用于为HTML文档添加样式和布局的最新版本的层叠样式表(Cascading Style Sheets)。下面是一些常用的CSS3属性及其详细解释: border-radius:设置元素的边框圆角的半径。可以使用四个值设置四个不同的圆角半径,也可…...
小程序:如何合理规划分包使主包不超过2M
背景 做过小程序项目的同学应该都有这样的经历,项目做着做着,突然发现代码包的大小超过了 2M,小程序无法提审,然后痛苦的删文件改代码来减少包大小。 虽然我们也知道小程序给我们提供了分包的功能可以减少主包的大小,…...

迭代器的封装与反向迭代器
一、反向迭代器 在list模拟实现的过程中,第一次接触了迭代器的封装,将list的指针封装成了一个新的类型,并且以迭代器的基本功能对其进行了运算符重载 反向迭代器是对正向迭代器的封装,并且体现了泛型编程的思想,任意…...
PHP项目学习笔记-萤火商城https://www.yiovo.com/doc
萤火商城学习笔记 注意事项关于建表增加页面流程前台页面的数据列表数据下拉列表的数据 关于时间的处理前台界面数据处理 多年没有碰过php代码了,这个项目不错,想好好学习下,持续更新 注意事项 打开APP_DEBUG有些时候改了前台页面后&#x…...

我国有多少个港口?
港口是什么? 港口是海洋运输中不可或缺的重要设施之一,是连接陆路和水路运输的重要节点。港口通常是指位于沿海地区的水陆交通枢纽,是船舶停靠、装卸货物、储存物资和维修船只的场所。港口一般由码头、泊位、仓库、货场、客运站等设施组成&a…...

uniapp实现登录组件之外区域置灰并引导登录
实现需求 每个页面需要根据用户是否登录决定是否显示登陆组件,登录组件半屏底部显示,登录组件之外区域置灰,功能按钮点击之后引导提示登录.页面效果如下: 实现思路说明 设置登录组件背景颜色为灰色,将页面分成登录区域(底部)和非登陆区域(上面灰色显示部分), 置灰区域添加…...

抄表系统是如何抄到电表水表的数据的?
抄表系统是一种利用无线通信技术,实现远程读取电表水表数据的系统。抄表系统主要由三部分组成:电表水表、集中器和后台管理平台。接下来,小编来为大家详细的介绍下抄表系统是如何抄到电表水表的数据的,一起来看下吧! 电表水表是抄…...

Qt之自定义事件QEvent
在Qt中,自定义事件的步骤大概如下: 1.创建自定义事件,自定义事件需要继承QEvent 2.使用QEvent::registerEventType()注册自定义事件类型,事件的类型需要在 QEvent::User 和 QEvent::MaxUser 范围之间,在QEvent::User之前是预留给系统的事件 3.使用sendEvent() 和 postEv…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...

STM32标准库-ADC数模转换器
文章目录 一、ADC1.1简介1. 2逐次逼近型ADC1.3ADC框图1.4ADC基本结构1.4.1 信号 “上车点”:输入模块(GPIO、温度、V_REFINT)1.4.2 信号 “调度站”:多路开关1.4.3 信号 “加工厂”:ADC 转换器(规则组 注入…...

MeshGPT 笔记
[2311.15475] MeshGPT: Generating Triangle Meshes with Decoder-Only Transformers https://library.scholarcy.com/try 真正意义上的AI生成三维模型MESHGPT来袭!_哔哩哔哩_bilibili GitHub - lucidrains/meshgpt-pytorch: Implementation of MeshGPT, SOTA Me…...

20250609在荣品的PRO-RK3566开发板的Android13下解决串口可以执行命令但是脚本执行命令异常的问题
20250609在荣品的PRO-RK3566开发板的Android13下解决串口可以执行命令但是脚本执行命令异常的问题 2025/6/9 20:54 缘起,为了跨网段推流,千辛万苦配置好了网络参数。 但是命令iptables -t filter -F tetherctrl_FORWARD可以在调试串口/DEBUG口正确执行。…...
更新 Docker 容器中的某一个文件
🔄 如何更新 Docker 容器中的某一个文件 以下是几种在 Docker 中更新单个文件的常用方法,适用于不同场景。 ✅ 方法一:使用 docker cp 拷贝文件到容器中(最简单) 🧰 命令格式: docker cp <…...

联邦学习带宽资源分配
带宽资源分配是指在网络中如何合理分配有限的带宽资源,以满足各个通信任务和用户的需求,尤其是在多用户共享带宽的情况下,如何确保各个设备或用户的通信需求得到高效且公平的满足。带宽是网络中的一个重要资源,通常指的是单位时间…...