Java多线程--线程间的通信
文章目录
- 一、线程间的通信
- (1)为什么要处理线程间的通信
- (2)等待唤醒机制
- 二、案例
- (1)案例
- 1、创建线程
- 2、解决线程安全问题
- 3、等待
- 4、唤醒
- 5、同步监视器
- (2)调用wait和notify需注意的细节
- 三、wait与sleep的区别
一、线程间的通信
(1)为什么要处理线程间的通信
当我们需要多个线程
来共同完成一件任务,并且我们希望他们有规律的执行
,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。(在同步的基础之上解决通信的问题)
比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信,即—— 等待唤醒机制。
(2)等待唤醒机制
这是多个线程间的一种协作机制
。
谈到线程我们经常想到的是线程间的竞争(race)
,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)
), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify()
);
或可以指定wait的时间,等时间到了自动唤醒;
在有多个线程进行等待时,如果需要,可以使用 notifyAll()
来唤醒所有的等待线程。wait/notify
就是线程间的一种协作机制。
- wait:线程不再活动,不再参与调度,进入
wait set
中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是 WAITING 或 TIMED_WAITING。它还要等着别的线程执行一个特别的动作
,也即“通知(notify)
”或者等待时间到,在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue
)中。 - notify:则选取所通知对象的 wait set 中的一个线程释放。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
🗳️注意:
被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
- 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
二、案例
(1)案例
🌋案例描述:
使用两个线程打印 1-100。线程1, 线程2 交替打印。
🍰分析
1、创建线程
这里使用实现的方式创建线程。
class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {}
}
在run
方法里面写上逻辑代码:
class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){if(number<=100){System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}
}
在main
方法中创建两个线程,如下:
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}
关于number的问题,需要考虑同步。
在if中加一个sleep
,将问题放大。
目前的代码如下:
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}
}
输出:
可以看到打印出了错误信息。
2、解决线程安全问题
现在我们使用同步代码块
来解决线程安全问题。
将操作number的代码用synchronized
包裹起来,就是下面蓝色部分:
用synchronized
包裹的时候,不能包裹少了,那样会有安全问题。
若是包裹多了,串行的场景就会更多,导致效率变低。
所以,操作共享数据的代码,不能包裹多,也不能少。
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}}
}
🍺输出
可以看到现在的线程是安全的。
3、等待
紧接着需要考虑下一个问题,需要将两个线程交替打印。
🎲如何实现交替?
比如线程1进入同步代码块,然后打印结束,出了同步代码块。
此时线程1需要进入阻塞状态,让线程2进入同步代码块输出执行,才能保证与线程1交替执行。
如何让线程1处于阻塞,这里需要使用一个方法wait()
,让它处于等待状态。如下:
线程一旦执行wait()
方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用。
wait()
方法有一个异常需要处理一下:
注意,sleep()
不会释放同步监视器,这一点需要注意。
有时候面试会问,wait()
与sleep()
有什么区别?
这就是其中一个区别,sleep()
不会释放对同步监视器的调用,而wait()
会释放对同步监视器的调用。
🌱代码
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出
☕注意
一旦线程1进入同步代码块执行到wait()
,就会释放对同步监视器的调用,然后开始等待操作。
线程2就可以拿着同步监视器进入同步代码块了,当线程2执行到wait()
,它也开始等待。
所以两个线程都处于等待状态,只能输出两个数字,然后进入了阻塞状态。
4、唤醒
🎲如何不一直等待下去呢?
Ctrl+P
可以看到wait()
方法还有带参的,可以设置等待时间,就是达到时间自动醒来。如下:
现在我们不使用这个了,因为想让它们交互输出,可以考虑唤醒线程。
线程1进入同步代码块执行到wait()
,然后释放同步监视器,线程2进入同步代码块。
所以我们需要在线程2wait()
之前把线程1给唤醒。
可以在这里写上notify()
,如下:
🍰分析一下现在的情况:
线程1进入同步代码块,碰到了notify()
,发现没有可以唤醒的线程,就继续往后执行。
然后线程1打印了“1”,接下来执行到wait()
,开始等待,并且释放同步监视器。
线程2拿到锁,进入同步代码块,碰到了notify()
,发现线程1在等待,就将线程1叫醒,虽然叫醒了,但是没有用,因为已经没有同步资源了。
线程2就拿着锁继续执行,即使sleep
也不影响自动睡醒之后继续往后执行(sleep不会导致锁被释放),然后输出“2”。
当线程2执行到wait()
,就开始等待了,同时也释放同步监视器。
此时线程1是醒着的状态,从被wait()
的地方继续往后执行,后边要是有代码的话还需要继续执行,然后拿着锁再次进入同步代码块,然后碰到notify()
,将线程2叫醒。
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出(部分)
现在看到的就是交互的场景。
5、同步监视器
🗳️关于同步监视器
这里其实省略了this
,如下:
wait
同理:
notify(); //this.notify();
wait(); //this.wait();
凡是在方法当中,没有写是谁调用的,如果是非静态方法,那就少了this。
若是静态方法,那就是当前类。
这里的this
必须是一样的吗?如下:
我们先来自己定义一个同步监视器,然后将obj
放入:
🌱代码
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;Object obj=new Object();@Overridepublic void run() {//打印数据while(true){synchronized (obj) { //obj是唯一的,线程安全this.notify(); //notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出
会报一个异常IllegalMonitorStateException
,这表示当前使用的同步监视器(即obj)和notify()
、wait()
方法的调用者不一致。
所以,它们的调用者,必须是同步监视器。(这里也可以看出,notify
和wait
方法必须要在同步代码块或同步方法中使用,只有这两个结构中才存在同步监视器,Lock里面没有同步监视器)
🚗处理
既然现在的同步监视器是obj,那么就用obj去调用notify
和wait
方法即可。
如下:
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;Object obj=new Object();@Overridepublic void run() {//打印数据while(true){synchronized (obj) { //obj是唯一的,线程安全obj.notify(); //notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {obj.wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出(部分)
notify
和wait
方法的调用者必须是“同步监视器”。
(2)调用wait和notify需注意的细节
上述案例涉及到三个方法的使用:
wait()
:线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用。notify()
:一旦执行此方法,就会唤醒被wait()
的线程中优先级最高的那一个线程。(如果被wait()
的多个线程的优先级相同,则随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
🗳️注意点
- 此三个方法的使用,必须是在同步代码块或同步方法(即
synchronized
结构)中。(超纲:Lock
需要配合Condition
实现线程间的通信,方式更加灵活) - 此三个方法的调用者,必须是同步监视器。否则,会报
IllegalMonitorStateException
异常。 - 此三个方法声明在Object类中。(当初说同步监视器的时候,说到“任何一个对象都可以来充当同步监视器”,那么就意味着任何一个对象都应该有能力去调用这几个方法,这几个方法必定是定义在Object类里面的)
☕调用wait
和notify
需注意的细节总结
1、wait
方法与notify
方法必须要由同一个锁对象调用
。
因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2、wait
方法与notify
方法是属于Object类的方法的。
因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3、wait
方法与notify
方法必须要在同步代码块
或者是同步函数
中使用。
因为:必须要通过锁对象
调用这2个方法。否则会报java.lang.IllegalMonitorStateException
异常。
三、wait与sleep的区别
🎲wait()
和 sleep()
的区别?(常见面试题)
①相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态。
②不同点:
- 声明的位置(定义方法所属的类)
wait()
:声明在Object类中。sleep()
:声明在Thread类中,静态的。
- 使用的场景不同(使用范围不同)
wait()
:只能使用在同步代码块或同步方法中。sleep()
:可以在任何需要使用的场景。
- 都在同步结构(同步代码块或同步方法)中使用的时候,是否释放同步监视器的操作不同
wait()
:一旦执行,会释放同步监视器。sleep()
:一旦执行,不会释放同步监视器。
- 结束阻塞的方式(结束等待的方式不同)
wait()
:到达指定时间自动结束阻塞 或 无限等待直到被notify/notifyAll唤醒,结束阻塞。sleep()
:到达指定时间自动结束阻塞。
相关文章:

Java多线程--线程间的通信
文章目录 一、线程间的通信(1)为什么要处理线程间的通信(2)等待唤醒机制 二、案例(1)案例1、创建线程2、解决线程安全问题3、等待4、唤醒5、同步监视器 (2)调用wait和notify需注意的…...
vue + element 页面滚动计算百分比 + 节流函数
html: <el-progress :percentage"scrollValue"></el-progress> js: data() {return {scrollValue: 0,} }, mounted() {window.addEventListener(scroll, this.handleScroll) // 监听页面滚动 }, beforeDestroy() {window.remov…...

【笔记】React Native实战练习(仿网易云游戏网页移动端)
/** * 如果系统看一遍RN相关官方文档,可能很快就忘记了。一味看文档也很枯燥无味, * 于是大概看了关键文档后,想着直接开发一个Demo出来,边学边写,对往后工作 * 开发衔接上能够更顺。这期间肯定会遇到各种各样的问题&a…...

Android SystemUI 介绍
目录 一、什么是SystemUI 二、SystemUI应用源码 三、学习 SystemUI 的核心组件 四、修改状态与导航栏测试 本篇文章,主要科普的是Android SystemUI , 下一篇文章我们将介绍如何把Android SystemUI 应用转成Android Studio 工程项目。 一、什么是Syst…...

2024美赛数学建模A题思路分析 - 资源可用性和性别比例
1 赛题 问题A:资源可用性和性别比例 虽然一些动物物种存在于通常的雄性或雌性性别之外,但大多数物种实质上是雄性或雌性。虽然许多物种在出生时的性别比例为1:1,但其他物种的性别比例并不均匀。这被称为适应性性别比例的变化。例…...

2024年数学建模美赛C题(预测 Wordle)——思路、程序总结分享
1: 问题描述与要求 《纽约时报》要求您对本文件中的结果进行分析,以回答几个问题。 问题1:报告结果的数量每天都在变化。开发一个模型来解释这种变化,并使用您的模型为2023年3月1日报告的结果数量创建一个预测区间。这个词的任何属性是否会…...

TryHackMe-File Inclusion练习
本文相关的TryHackMe实验房间链接:TryHackMe | Why Subscribe 路径遍历(目录遍历) LocationDescription/etc/issue包含要在登录提示之前打印的消息或系统标识。/etc/profile控制系统范围的默认变量,例如导出(Export)变量、文件创…...
Leetcode 《面试经典150题》169. 多数元素
题目 给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的,并且给定的数组总是存在多数元素。 示例 1: 输入:nums [3,2,3] 输出:3示…...

百度输入法往选字框里强塞广告
关注卢松松,会经常给你分享一些我的经验和观点。 国内几乎100%的输入法都有广告,只是你们没发现而已!!! 百度输入法居然在输入法键盘上推送广告,近日,博主阑夕 表示,V2EX论坛上有…...
分享一个Qt使用的模块间通信类
需求: 不同线程,或者同一线程的不同类之间通信,按照Qt的机制,定义一个信号,一个槽,然后绑定。以两个类A,B为例,A触发一个信号,B执行一个槽,在定义好信号和槽之后&#x…...

工作七年,对消息推送使用的一些经验和总结
前言:不管是APP还是WEB端都离不开消息推送,尤其是APP端,push消息,小信箱消息;WEB端的代办消息等。因在项目中多次使用消息推送且也是很多项目必不可少的组成部分,故此总结下供自己参考。 一、什么是消息推…...

计网——应用层
应用层 应用层协议原理 网络应用的体系结构 客户-服务器(C/S)体系结构 对等体(P2P)体系结构 C/S和P2P体系结构的混合体 客户-服务器(C/S)体系结构 服务器 服务器是一台一直运行的主机,需…...

算法面试八股文『 基础知识篇 』
博客介绍 近期在准备算法面试,网上信息杂乱不规整,出于强迫症就自己整理了算法面试常出现的考题。独乐乐不如众乐乐,与其奖励自己,不如大家一起嗨。以下整理的内容可能有不足之处,欢迎大佬一起讨论。 PS:…...

docker-学习-4
docker学习第四天 docker学习第四天1. 回顾1.1. 容器的网络类型1.2. 容器的本质1.3. 数据的持久化1.4. 看有哪些卷1.5. 看卷的详细信息 2. 如何做多台宿主机里的多个容器之间的数据共享2.1. 概念2.2. 搭NFS服务器实现多个容器之间的数据共享的详细步骤2.3. 如果是多台机器&…...

el-upload子组件上传多张图片(上传为files或base64url)
场景: 在表单页,有图片需要上传,表单的操作行按钮中有上传按钮,点击上传按钮。 弹出el-dialog进行图片的上传,可以上传多张图片。 由于多个表单页都有上传多张图片的操作,因此将上传多图的el-upload定义…...
2024美赛数学建模C题思路源码——网球选手的动量
这题挺有意思,没具体看比赛情况,打过比赛的人应该都知道险胜局(第二局、第五局逆转局)最影响心态的,导致第3、5局输了 模型结果需要证明这样的现象 赛题目的 赛题目的:分析网球球员的表现 问题一.球员在比赛特定时间表现力 问题分析 excel数据:每个时间段有16场比赛,…...

金三银四_程序员怎么写简历_写简历网站
你们在制作简历时,是不是基本只关注两件事:简历模板,还有基本信息的填写。 当你再次坐下来更新你的简历时,可能会发现自己不自觉地选择了那个“看起来最好看的模板”,填写基本信息,却没有深入思考如何使简历更具吸引力。这其实是一个普遍现象:许多求职者仍停留在传统简历…...

echarts条形图添加滚动条
效果展示: 测试数据: taskList:[{majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {maj…...
Java 使用Soap方式调用WebService接口
pom文件依赖 <dependencies><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.13.0</version></dependency><!-- https://mvnrepository.com/artif…...
2024美赛数学建模所有题目思路分析
美赛思路已更新,关注后可以获取更多思路。并且领取资料 C题思路 首先,我们要理解势头是什么。简单来说,势头是一方在比赛中因一系列事件而获得的动力或优势。在网球中,这可能意味着连续赢得几个球,或是在比赛的某个关…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...

【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...

C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...