并发编程(六)—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、设置动态…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...

【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...

零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...