并发编程(六)—AbstractExecutorService源码分析
一、AbstractExecutorService简介
AbstractExecutorService是一个抽象类,实现了ExecutorService接口,提供了线程池的基本实现。它是Java Executor框架的核心类,提供了线程池的基本操作,如提交任务、管理线程池、执行任务等。
自定义执行器服务可用扩展AbreactExecutorService 并覆盖其方法以提供自己的实现。这使开发人员可以创建适合其特定需求的执行器服务。
二、AbstractExecutorService如何使用
AbstractExecutorService是一个抽象类,不能直接实例化,需要通过继承它的子类ThreadPoolExecutor或ScheduledThreadPoolExecutor来使用。使用AbstractExecutorService创建线程池的步骤如下:
2.1 没有返回值
// 1、创建ThreadPoolExecutor 创建一个固定大小的线程池,核心线程数为1,最大线程数为5,任务队列大小为10
ExecutorService executorService = new ThreadPoolExecutor(1, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));
// 2、提交任务
executorService.submit(new Runnable() {@Overridepublic void run() {//执行任务}
});
// 3、关闭线程池
executorService.shutdown();
2.2 有返回值
// 1、创建ThreadPoolExecutor 创建一个固定大小的线程池,核心线程数为1,最大线程数为5,任务队列大小为10
ExecutorService executorService = new ThreadPoolExecutor(1, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));
// 2、提交一个Callable任务
Future<String> future = executorService.submit(new Callable<String>() {@Overridepublic String call() throws Exception {//执行任务,返回结果return "hello ExecutorService";}
});
// 3、获取值
String result = future.get();
// 4、关闭线程池
executorService.shutdown();
总之,使用AbstractExecutorService创建线程池非常简单,只需要创建线程池对象、提交任务、关闭线程池即可。同时,AbstractExecutorService也提供了一些高级功能,如异步任务、定时任务等,可以根据具体需求选择使用。
三、如何自定义AbstractExecutorService
要自定义一个延迟AbstractExecutorService,以下步骤:
继承AbstractExecutorService类
实现 submit 方法 覆盖 submit 方法,将任务添加到队列中,其中在任务执行前应用延迟。
实现 execute 方法 覆盖 execute 方法,调用 submit 方法并返回 Future 对象,等待任务完成。
实现shutdown 方法 覆盖 shutdown 方法,停止所有任务并等待它们完成。
实现 DelayedTask 类 创建一个 DelayedTask 类,用于封装任务和延迟时间。
实现 RunnableTask 和 CallableTask 类 创建 RunnableTask 和 CallableTask 类,用于封装任务。
public class CustomDelayedExecutorService extends AbstractExecutorService {private final DelayQueue<DelayedTask<?>> queue = new DelayQueue<>();@Overridepublic <T> Future<T> submit(Callable<T> task) {return schedule(new CallableTask<>(task));}@Overridepublic Future<?> submit(Runnable task) {return schedule(new RunnableTask(task));}private <T> Future<T> schedule(DelayedTask<T> task) {queue.offer(task);return task;}@Overridepublic void execute(Runnable command) {submit(command);}@Overridepublic void shutdown() {for (DelayedTask<?> task : queue) {task.cancel(false);}}@Overridepublic List<Runnable> shutdownNow() {List<Runnable> tasks = new ArrayList<>();for (DelayedTask<?> task : queue) {if (task.cancel()) {tasks.add(task.getTask());}}return tasks;}@Overridepublic boolean isShutdown() {return false;}@Overridepublic boolean isTerminated() {return false;}@Overridepublic boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {return false;}
}
class DelayedTask<T> implements RunnableFuture<T>, Delayed {private final long delay;private final long expire;private final RunnableFuture<T> task;public DelayedTask(RunnableFuture<T> task, long delay) {this.task = task;this.delay = delay;this.expire = System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, TimeUnit.MILLISECONDS);}@Overridepublic long getDelay(TimeUnit unit) {return unit.convert(expire - System.nanoTime(), TimeUnit.NANOSECONDS);}@Overridepublic int compareTo(Delayed o) {return Long.compare(expire, ((DelayedTask<?>) o).expire);}@Overridepublic void run() {if (!isCancelled()) {task.run();}}@Overridepublic boolean cancel(boolean mayInterruptIfRunning) {return task.cancel(mayInterruptIfRunning);}@Overridepublic boolean isCancelled() {return task.isCancelled();}@Overridepublic boolean isDone() {return task.isDone();}@Overridepublic T get() throws InterruptedException, ExecutionException {return task.get();}@Overridepublic T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {return task.get(timeout, unit);}public RunnableFuture<T> getTask() {return task;}
}
class RunnableTask implements RunnableFuture<Void> {private final Runnable task;public RunnableTask(Runnable task) {this.task = task;}@Overridepublic void run() {task.run();}@Overridepublic boolean cancel(boolean mayInterruptIfRunning) {return false;}@Overridepublic boolean isCancelled() {return false;}@Overridepublic boolean isDone() {return true;}@Overridepublic Void get() throws InterruptedException, ExecutionException {return null;}@Overridepublic Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {return null;}
}
class CallableTask<T> extends FutureTask<T> {public CallableTask(Callable<T> callable) {super(callable);}
}
四、AbstractExecutorService源码分析
4.1 AbstractExecutorService dragrams

4.2 方法分析

4.2.1 newTaskFor(Runnable runnable, T value) 方法
AbstractExecutorService中的newTaskFor方法,用于创建一个RunnableFuture对象,其中RunnableFuture是Future和Runnable接口的结合体,可以用来表示异步执行的结果。该方法的参数包括一个Runnable对象和一个泛型T的值。通过传入的Runnable对象和T类型的值,创建了一个FutureTask对象,并将其返回。
FutureTask是RunnableFuture的一个具体实现,它表示一个可取消的异步计算,可以通过调用get方法获取计算结果。在创建FutureTask对象时,需要传入一个Callable对象或Runnable对象,用于执行异步计算。而在这里,我们传入了一个Runnable对象和一个T类型的值,表示该Runnable对象的执行结果为T类型的值。
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {// 将runnable包装成FutureTask类型return new FutureTask<T>(runnable, value);}
4.2.2 submit(Runnable task)方法

AbstractExecutorService中的submit方法是ExecutorService接口中的一个方法,用于提交一个Runnable任务,并返回一个表示该任务异步执行结果的Future对象。
如果传入的task为null,则抛出NullPointerException异常;
调用newTaskFor(Runnable runnable, T value)方法创建一个RunnableFuture对象ftask,其中RunnableFuture是Future和Runnable接口的结合体,用于表示异步执行的结果。在该方法中,我们将泛型T的值设置为null,表示该Runnable任务没有返回值;
通过调用execute(Runnable command)方法提交任务进行执行,该方法是一个抽象方法,需要由子类实现。在该方法中,我们将创建的ftask对象作为参数传入,表示需要执行的任务为ftask对象。
最后,将ftask对象返回,作为Future对象,用于获取任务执行结果或取消任务。
public Future<?> submit(Runnable task) {// 如果任务为null,则抛出nullPointerExceptionif (task == null) throw new NullPointerException();// 将task包装成RunnableFutureRunnableFuture<Void> ftask = newTaskFor(task, null);// 通过调用execute(Runnable command)方法提交任务进行执行execute(ftask);// 返回RunnableFuturereturn ftask;}
4.2.3 doInvokeAny(Collection<? extends Callable<T>> tasks,boolean timed, long nanos)方法

AbstractExecutorService中doInvokeAny方法用于执行一个任务集合中的所有任务并返回第一个成功执行的任务的结果,如果所有任务都执行失败则抛出ExecutionException异常。其中,方法参数tasks为任务集合,timed和nanos用于指定超时时间,如果超时则抛出TimeoutException异常。
如果任务集合是否为空,则抛出NullPointerException异常
如果任务集合的大小为0,则抛出IllegalArgumentException异常
futures用于存储所有任务的Future对象, ecs对象用于异步执行任务集合中的任务。
从迭代器中获取一个任务xecutorCompletionService,并将该任务的执行结果的Future添加到futures列表中
死循环不断从ExecutorCompletionService中获取已完成的任务的结果Future,直到有一个任务完成为止,具体细流程如下:
调用ExecutorCompletionService的poll()方法获取一个已完成的任务的结果Future,如果poll()返回null,表示当前没有已完成的任务。
当前没有已完成的任务
还有未执行的任务(ntasks > 0),则从任务集合中获取下一个任务,并将其提交给ExecutorCompletionService执行。提交成功后,活跃线程数active加一;
没有执行的任务,并且活跃线程数为0(即所有任务都已执行完毕),则退出循环;
指定超时时间(timed为true),则调用poll(nanos, TimeUnit.NANOSECONDS)方法获取已完成的任务的结果Future,并等待最多nanos纳秒的时间。如果超时则抛出TimeoutException;
没有指定超时时间,则调用take()方法阻塞等待已完成的任务的结果Future。
如果获取到了已完成的任务的结果Future,则将活跃线程数active减一,并尝试获取该任务的执行结果。
如果获取结果时出现异常,则将异常保存在ExecutionException中,继续下一轮循环;
如果成功获取到了一个任务的执行结果,则直接返回该结果。
如果所有任务都已经完成,但是没有找到任何一个成功的任务,则抛出ExecutionException异常。
最后,取消所有尚未完成的任务,以便节省资源并提高效率
private<T> T doInvokeAny(Collection<? extends Callable<T>> tasks,boolean timed, long nanos)throws InterruptedException, ExecutionException, TimeoutException {// 如果任务集合是否为空,则抛出NullPointerException异常if (tasks == null)throw new NullPointerException();int ntasks = tasks.size();// 如果任务集合的大小为0,则抛出IllegalArgumentException异常if (ntasks == 0)throw new IllegalArgumentException();// futures用于存储所有任务的Future对象 ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);ExecutorCompletionService<T> ecs =new ExecutorCompletionService<T>(this);try {ExecutionException ee = null;// 计算deadline时间final long deadline = timed ? System.nanoTime() + nanos : 0L;Iterator<? extends Callable<T>> it = tasks.iterator();// 从迭代器中获取一个任务xecutorCompletionService,并将该任务的执行结果的Future添加到futures列表中。futures.add(ecs.submit(it.next()));// 任务数减1--ntasks;// 退出循环标识(也就是调用ecs.sumbit的任务数)int active = 1;// 自旋、死循环for (;;) {// 获取Future对象Future<T> f = ecs.poll();// 判断future 任务是否已完成if (f == null) {// 任务没有完成,继续判断是否还有未执行的任务if (ntasks > 0) {// 任务s数减1--ntasks;// 再从tasks中获取一个任务,提交到ExecutorCompletionService中进行执行。futures.add(ecs.submit(it.next()));// 提交ecs中加1++active;}else if (active == 0)break;// 有超时时间限制 else if (timed) {//调用ExecutorCompletionService的poll(long timeout, TimeUnit unit)方法等待指定的超时时间。f = ecs.poll(nanos, TimeUnit.NANOSECONDS);// 在等待过程中已经超过了指定的超时时间,因此会抛出 TimeoutException 异常if (f == null)throw new TimeoutException();// 将剩余的超时时间重新计算,并继续执行后续的代码。nanos = deadline - System.nanoTime();}// 如果没有超时时间限制,则调用ExecutorCompletionService的take()方法一直等待,直到有任务执行完成。elsef = ecs.take(); }// 任务已完成(获取到Future对象)if (f != null) {--active;try {// 尝试获取Future对象的执行结果,return f.get();} catch (ExecutionException eex) {// 如果获取失败,则将异常保存在ee变量中,继续等待下一个任务的执行结果ee = eex;} catch (RuntimeException rex) {ee = new ExecutionException(rex);}}}// 如果所有任务都已经完成,但是没有找到任何一个成功的任务,则抛出ExecutionException异常。if (ee == null)ee = new ExecutionException();throw ee;} finally {// 取消所有尚未完成的任务,以便节省资源并提高效率for (int i = 0, size = futures.size(); i < size; i++)futures.get(i).cancel(true);}}
4.2.4 invokeAll(Collection<? extends Callable<T>> tasks)方法

AbstractExecutorService中的invokeAll方法,它的作用是在执行给定的任务集合tasks中的所有任务,并等待所有任务完成后返回一个包含Future对象的列表,Future对象可以用来获取每个任务的执行结果。如果其中某个任务抛出异常,则该异常将传播到调用者。
判断任务是否为空,如果空,则抛出NullPointerException异常;
遍历参数tasks中的每个Callable任务,将每个任务转换成一个RunnableFuture对象,并把任务添加到futures中,然后调用execute方法执行任务。
循环遍历futures中的每个Future对象,如果某个Future对象的任务还没有完成,则调用get()方法等待任务完成,如果任务抛出异常,则忽略异常。
如果所有的任务都成功完成,则将done标记为true,并返回futures;
否则,取消所有未完成的任务,并抛出InterruptedException异常。
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)throws InterruptedException {// 判断任务是否为空,如果空,则抛出NullPointerException异常;if (tasks == null)throw new NullPointerException();ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());boolean done = false;try {// 遍历参数tasks中的每个Callable任务for (Callable<T> t : tasks) {// 将任务转换成一个RunnableFuture对象RunnableFuture<T> f = newTaskFor(t);futures.add(f);// 调用execute方法执行任务execute(f);}// 循环遍历futures中的每个Future对象for (int i = 0, size = futures.size(); i < size; i++) {Future<T> f = futures.get(i);// 如果某个Future对象的任务还没有完成if (!f.isDone()) {try {// 调用get()方法等待任务完成,如果任务抛出异常,则忽略异常。f.get();} catch (CancellationException ignore) {} catch (ExecutionException ignore) {}}}// 如果所有的任务都成功完成,则将done标记为true,并返回futures;done = true;return futures;} finally {if (!done)// 如果所有的任务没有全部成功完成,取消所有未完成的任务for (int i = 0, size = futures.size(); i < size; i++)futures.get(i).cancel(true);}
}
相关文章:

并发编程(六)—AbstractExecutorService源码分析
一、AbstractExecutorService简介AbstractExecutorService是一个抽象类,实现了ExecutorService接口,提供了线程池的基本实现。它是Java Executor框架的核心类,提供了线程池的基本操作,如提交任务、管理线程池、执行任务等。自定义…...

015行为型-职责链模式
目录定义标准模式实现:职责链变体使用链表实现使用数组实现应用场景日志输出spring过滤器spirng 拦截器mybatis动态sql定义 责链模式是一种设计模式,其目的是使多个对象能够处理同一请求,但是并不知道下一个处理请求的对象是谁。它能够解耦请…...

python例程:五子棋(控制台版)程序
目录《五子棋(控制台版)》程序使用说明程序示例代码可执行程序及源码下载路径《五子棋(控制台版)》程序使用说明 在PyCharm中运行《五子棋(控制台版)》即可进入如图1所示的系统主界面。 图1 游戏主界面 具…...

leveldb的Compaction线程
个人随笔 (Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 1. leveldb的Compaction全局线程 在leveldb中,有一个全局的后台线程BGThread,用于数据库的MinorCompact与MajorCompact。 重点关注“全局线程”: 这个标识着无论一个进程打开…...

邪恶的想法冒出,立马启动python实现美女通通下
前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! 完整源码、python资料: 点击此处跳转文末名片获取 当我在首页刷到这些的时候~ 我的心里逐渐浮现一个邪念:我把这些小姐姐全都采集,可以嘛? 答案当然是可以的~毕竟就我这技术,…...

蓝桥杯刷题冲刺 | 倒计时18天
作者:指针不指南吗 专栏:蓝桥杯倒计时冲刺 🐾马上就要蓝桥杯了,最后的这几天尤为重要,不可懈怠哦🐾 文章目录0.知识点1.乳草的入侵今天写 搜索题 0.知识点 DFS 设计步骤 确定该题目的状态(包括边…...

经典算法面试题——Java篇-附带赠书活动,评论区随机选取一人赠书
目录 一.图书推荐 二.说一下什么是二分法?使用二分法时需要注意什么?如何用代码实现? 三.什么是插入排序?用代码如何实现? 四.什么是冒泡排序?用代码如何实现? 五.什么是斐波那契数列&#…...

支持RT-Thread最新版本的瑞萨RA2E1开发板终于要大展身手了
支持RT-Thread最新版本的瑞萨RA2E1开发板终于要大展身手了 熟悉RT-Thread和瑞萨MCU的朋友都知道,当前RT-Thread仓库的主线代码是不支持RA2E1这个BSP的。刚好,最近我在联合瑞萨推广一个叫《致敬未来的攻城狮计划》,使用的就是RA2E1开发板&…...

【C语言进阶】 12. 假期测评①
day01 1. 转义字符的判断 以下不正确的定义语句是( ) A: double x[5] {2.0, 4.0, 6.0, 8.0, 10.0}; B: char c2[] {‘\x10’, ‘\xa’, ‘\8’}; C: char c1[] {‘1’,‘2’,‘3’,‘4’,‘5’}; D: int y[53]{0, 1, 3, 5, 7, 9}; 【答案解析】 B 本…...

给程序加个进度条吧,1行Python代码,快速添加~
大家好,这里是程序员晚枫。 你在写代码的过程中,有没有遇到过以下问题? 已经写好的程序,想看看程序执行的进度? 在写代码批量处理文件的时候,如何显示现在处理到第几个文件了? 👆…...

常见的Keil5编译报错及其原因和解决方法
以下是几种常见的Keil5编译报错及其原因和解决方法: "Error: L6218E: Undefined symbol"(未定义符号错误) 这通常是由于缺少对应的库文件或者代码中有未声明的变量或函数引起的。解决方法是检查相应的库文件是否已正确添加到工程中…...

Django 实现瀑布流
需求分析 现在是 "图片为王"的时代,在浏览一些网站时,经常会看到类似于这种满屏都是图片。图片大小不一,却按空间排列,就这是瀑布流布局。 以瀑布流形式布局,从数据库中取出图片每次取出等量(7 …...

传输层协议----UDP/TCP
文章目录前言一、再谈端口号端口号的划分认识知名端口号(Well-Know Port Number)两个问题nestatpidof二、UDP协议UDP协议端格式UDP的特点面向数据报UDP的缓冲区UDP使用注意事项基于UDP的应用层协议二、TCP协议TCP协议段格式可靠性问题确认应答(ACK)机制流量控制六个标志位PSHUG…...

教你如何快速在Linux中找到某个目录中最大的文件
工作中经常会有查看某个目录下最大的文件的需求,比如在运维工作中,发现某个系统或功能不工作了,经排查发现是服务器空间满了…那么接下来就需要清理一下临时文件或者日志文件,或者其他不需要的文件,那么就会想要查看一…...

Java二叉树面试题讲解
Java二叉树面试题讲解🚗1.检查两颗树是否相同🚕2.另一颗树的子树🚙3.二叉树最大深度🚌4.判断一颗二叉树是否是平衡二叉树🚎5.对称二叉树🚓6.获取树中结点个数🚑7.判断一个树是不是完全二叉树&am…...

rancher2.6进阶之nfs动态创建pv配置
添加NFS client provisioner 动态提供K8s后端存储卷 1.1.前提说明 1.1.1.说明 NFS client provisioner 利用 NFS Server 给 Kubernetes 作为持久存储的后端,并且动态提供PV。 默认 rancher 2 的存储类中的提供者不包含NFS,需要手动添加;添加方式有两种: 1)从应用商店直接安…...

快速上手vue elementUI好看的登录界面
这是一个非常非常适合新手的vue登录界面,总体来说美观大气,axios那部分没有发,有需要的大家可以自己进行二次开发,继续编写。 用到了技术栈有 vue/cli 5.07 element-ui 2.15.9 适合入门级新手,展示下页面 emmm验证码…...

Vue趣味【Vue3+Element Plus+Canvas实现一个简易画板;支持导出为图片】
目录🌟前言🌟粉丝先看🌟创建Vue3项目🌟引入Element Plus🌟实现代码(详细注释)🌟写在最后🌟JSON包里写函数,关注博主不迷路🌟前言 哈喽小伙伴们&a…...

【Spring Cloud Alibaba】2.服务注册与发现(Nacos安装)
文章目录环境要求简介安装Nacos源码安装Docker安装数据库配置访问服务我们要搭建一个Spring Cloud Alibaba项目就绕不开Nacos,阿里巴巴提供的Nacos组件,可以提供服务注册与发现和分布式配置服务,拥有着淘宝双十一十几年的流量经验,…...

深度学习 Day28——利用Pytorch实现好莱坞明星识别
深度学习 Day28——利用Pytorch实现好莱坞明星识别 文章目录深度学习 Day28——利用Pytorch实现好莱坞明星识别一、前言二、我的环境三、前期工作1、导入依赖项设置GPU2、导入数据集3、划分数据集四、调用官方的VGG16模型五、训练模型1、编写训练函数2、编写测试函数3、设置动态…...

Android中使用FCM进行消息推送
Firebase Cloud Message 的介绍 Firebase Cloud Message(FCM)是由Google推出的一种云端消息推送服务,它是由Google推出的Google Cloud Messaging(GCM)服务的升级版。在2016年5月,Google宣布将Google Cloud Messaging重命名为Firebase Cloud Message,作为Firebase的一部…...

从 X 入门Pytorch——BN、LN、IN、GN 四种归一化层的代码使用和原理
Pytorch中四种归一化层的原理和代码使用前言1 Batch Normalization(2015年提出)Pytorch官网解释原理Pytorch代码示例2 Layer Normalization(2016年提出)Pytorch官网解释原理Pytorch代码示例3 Instance Normalization(2…...

Windows环境下实施域名访问的一些小知识
文章目录 前言一、windows域名访问流程二、网络域名访问配置设置DNS未正确设置DNS的结果三、本地hosts设置本地hosts本地hosts的优先机制本地hosts的内部访问次序示例一示例二总结前言 作为一种常见的操作系统,windows系统具有其特殊的域名访问管理机制。了解其访问机制,将有…...

78.qt QCustomPlot介绍
参考https://www.qcustomplot.com/index.php/tutorials/settingup 下载地址: https://www.qcustomplot.com/index.php/download 1.添加帮助文档 在QtCreator ——>工具——>选项——>帮助——>文档——>添加,选择qcustomplot.qch文件,确定,以后按F1就能跳转到…...

win32api之文件系统管理(七)
什么是文件系统 文件系统是一种用于管理计算机存储设备上文件和目录的机制。文件系统为文件和目录分配磁盘空间,管理文件和目录的存储和检索,以及提供对它们的访问和共享,以下是常见的两种文件系统: NTFSFAT32磁盘分区容量2T32G…...

点云规则格网化,且保存原始的点云索引
点云规则格网化,且保存原始的点云索引 点云深度学习Voxelize规则,参考PTV2:https://github.com/Gofinge/PointTransformerV2 1总执行文件 import numpy as np import torch from pcr.utils.registry import Registry TRANSFORMS Registry…...

入职第一天就被迫离职,找工作多月已读不回,面试拿不到offer我该怎么办?
大多数情况下,测试员的个人技能成长速度,远远大于公司规模或业务的成长速度。所以,跳槽成为了这个行业里最常见的一个词汇。 前言 前几天,我们一个粉丝跟我说,正常入职一家外包,什么都准备好了࿰…...

走进Vue【三】vue-router详解
目录🌟前言🌟路由🌟什么是前端路由?🌟前端路由优点缺点🌟vue-router🌟安装🌟路由初体验1.路由组件router-linkrouter-view2.步骤1. 定义路由组件2. 定义路由3. 创建 router 实例4. 挂…...

html+css制作
<!DOCTYPE html> <html><head><meta charset"utf-8"><title>校园官网</title><style type"text/css">*{padding: 0;margin: 0;}#logo{width:30%;float: left;}.nav{width: 100%;height: 100px;background-color…...

Python实现rar、zip和7z文件的压缩和解压
一、7z压缩文件的压缩和解压 1、安装py7zr 我们要先安装py7zr第三方库: pip install py7zr如果python环境有问题,执行上面那一条安装语句老是安装在默认的python环境的话,我们可以执行下面这条语句,将第三方库安装在项目的虚拟…...