调度线程池ScheduledThreadPoolExecutor源码解析

news/2024/5/9 4:33:56/文章来源:https://blog.csdn.net/Huangjiazhen711/article/details/127528909

实现机制分析

我们先思考下,如果让大家去实现ScheduledThreadPoolExecutor可以周期性执行任务的功能,需要考虑哪些方面呢?

  1. ScheduledThreadPoolExecutor的整体实现思路是什么呢?

答: 我们是不是可以继承线程池类,按照线程池的思路,将任务先丢到阻塞队列中,等到时间到了,工作线程就从阻塞队列获取任务执行。

  1. 如何实现等到了未来的时间点就开始执行呢?

答: 我们可以根据参数获取这个任务还要多少时间执行,那么我们是不是可以从阻塞队列中获取任务的时候,通过条件队列的的awaitNanos(delay)方法,阻塞一定时间。

  1. 如何实现 任务的重复性执行呢?

答:这就更加简单了,任务执行完成后,把她再次加入到队列不就行了吗。

源码解析

类结构图

ScheduledThreadPoolExecutor的类结构图如上图所示,很明显它是在我们的线程池ThreadPoolExecutor框架基础上扩展的。

  • ScheduledExecutorService:实现了该接口,封装了调度相关的API
  • ThreadPoolExecutor:继承了该类,保留了线程池的能力和整个实现的框架
  • DelayedWorkQueue:内部类,延迟阻塞队列。
  • ScheduledFutureTask:延迟任务对象,包含了任务、任务状态、剩余的时间、结果等信息。

重要属性

通过ScheduledThreadPoolExecutor类的成员属性,我们可以了解它的数据结构。

  1. shutdown 后是否继续执行周期任务(重复执行)
private volatile boolean continueExistingPeriodicTasksAfterShutdown;
复制代码
  1. shutdown 后是否继续执行延迟任务(只执行一次)
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
复制代码
  1. 调用cancel()方法后,是否将该任务从队列中移除,默认false
private volatile boolean removeOnCancel = false;
复制代码
  1. 任务的序列号,保证FIFO队列的顺序,用来比较优先级
private static final AtomicLong sequencer = new AtomicLong()
复制代码
  1. ScheduledFutureTask延迟任务类
  • ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask
  • 该类具有延迟执行的特点, 覆盖 FutureTaskrun 方法来实现对延时执行、周期执行的支持。
  • 对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。
  • 成员属性如下:
// 任务序列号
private final long sequenceNumber;
// 任务可以被执行的时间,交付时间,以纳秒表示
private long time;	
// 0 表示非周期任务
// 正数表示 fixed-rate(两次开始启动的间隔)模式的周期,
// 负数表示 fixed-delay(一次执行结束到下一次开始启动) 模式
private final long period;	
// 执行的任务对象
RunnableScheduledFuture<V> outerTask = this;
// 任务在队列数组中的索引下标, -1表示删除
int heapIndex;
复制代码
  1. DelayedWorkQueue延迟队列
  • DelayedWorkQueue 是支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue(小根堆、满二叉树)存储元素。
  • 内部数据结构是数组,所以延迟队列出队头元素后需要让其他元素(尾)替换到头节点,防止空指针异常。
  • 成员属性如下:
// 初始容量
private static final int INITIAL_CAPACITY = 16;	
// 节点数量
private int size = 0;
// 存放任务的数组
private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY];	
// 控制并发用的锁
private final ReentrantLock lock = new ReentrantLock();	
// 条件队列
private final Condition available = lock.newCondition();
//指定用于等待队列头节点任务的线程
private Thread leader = null;
复制代码

提交延迟任务schedule()原理

延迟执行方法,并指定延迟执行的时间,只会执行一次。

  1. schedule()方法是延迟任务方法的入口。
public ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit) {// 判空处理if (command == null || unit == null)throw new NullPointerException();// 将外部传入的任务封装成延迟任务对象ScheduledFutureTaskRunnableScheduledFuture<?> t = decorateTask(command,new ScheduledFutureTask<Void>(command, null,triggerTime(delay, unit)));// 执行延迟任务delayedExecute(t);return t;
}
复制代码
  1. decorateTask(...) 该方法是封装延迟任务
  • 调用triggerTime(delay, unit)方法计算延迟的时间。
// 返回【当前时间 + 延迟时间】,就是触发当前任务执行的时间
private long triggerTime(long delay, TimeUnit unit) {// 设置触发的时间return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
long triggerTime(long delay) {// 如果 delay < Long.Max_VALUE/2,则下次执行时间为当前时间 +delay// 否则为了避免队列中出现由于溢出导致的排序紊乱,需要调用overflowFree来修正一下delayreturn now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}// 下面这种情况很少,大家看不懂可以不用强行理解
// 如果某个任务的 delay 为负数,说明当前可以执行(其实早该执行了)。
// 阻塞队列中维护任务顺序是基于 compareTo 比较的,比较两个任务的顺序会用 time 相减。
// 那么可能出现一个 delay 为正数减去另一个为负数的 delay,结果上溢为负数,则会导致 compareTo 产生错误的结果
private long overflowFree(long delay) {Delayed head = (Delayed) super.getQueue().peek();if (head != null) {long headDelay = head.getDelay(NANOSECONDS);// 判断一下队首的delay是不是负数,如果是正数就不用管,怎么减都不会溢出// 否则拿当前 delay 减去队首的 delay 来比较看,如果不出现上溢,排序不会乱// 不然就把当前 delay 值给调整为 Long.MAX_VALUE + 队首 delayif (headDelay < 0 && (delay - headDelay < 0))delay = Long.MAX_VALUE + headDelay;}return delay;
}
复制代码
  • 调用RunnableScheduledFuture的构造方法封装为延迟任务
ScheduledFutureTask(Runnable r, V result, long ns) {super(r, result);// 任务的触发时间this.time = ns;// 任务的周期, 延迟任务的为0,因为不需要重复执行this.period = 0;// 任务的序号 + 1this.sequenceNumber = sequencer.getAndIncrement();
}
复制代码
  • 调用decorateTask()方法装饰延迟任务
// 没有做任何操作,直接将 task 返回,该方法主要目的是用于子类扩展
protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable, RunnableScheduledFuture<V> task) {return task;
}
复制代码

提交周期任务scheduleAtFixedRate()原理

按照固定的评率周期性的执行任务,捕手renwu,一次任务的启动到下一次任务的启动的间隔

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,TimeUnit unit) {if (command == null || unit == null)throw new NullPointerException();if (period <= 0)throw new IllegalArgumentException();// 任务封装,【指定初始的延迟时间和周期时间】ScheduledFutureTask<Void> sft =new ScheduledFutureTask<Void>(command, null,triggerTime(initialDelay, unit), unit.toNanos(period));// 默认返回本身RunnableScheduledFuture<Void> t = decorateTask(command, sft);sft.outerTask = t;// 开始执行这个任务delayedExecute(t);return t;
}
复制代码

提交周期任务scheduleWithFixedDelay()原理

按照指定的延时周期性执行任务,上一个任务执行完毕后,延时一定时间,再次执行任务。

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit) {if (command == null || unit == null) throw new NullPointerException();if (delay <= 0)throw new IllegalArgumentException();// 任务封装,【指定初始的延迟时间和周期时间】,周期时间为 - 表示是 fixed-delay 模式ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null,triggerTime(initialDelay, unit), unit.toNanos(-delay));RunnableScheduledFuture<Void> t = decorateTask(command, sft);sft.outerTask = t;// 开始执行这个任务delayedExecute(t);return t;
}
复制代码

执行任务delayedExecute(t)原理

上面多种提交任务的方式,殊途同归,最终都会调用delayedExecute()方法执行延迟或者周期任务。

  • delayedExecute()方法是执行延迟任务的入口
private void delayedExecute(RunnableScheduledFuture<?> task) {// 线程池是 SHUTDOWN 状态,执行拒绝策略if (isShutdown())// 调用拒绝策略的方法reject(task);else {// 把当前任务放入阻塞队列super.getQueue().add(task);// 线程池状态为 SHUTDOWN 并且不允许执行任务了,就从队列删除该任务,并设置任务的状态为取消状态// 非主流程,可以跳过,不重点看了if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task))task.cancel(false);else// 开始执行了哈ensurePrestart();}
}
复制代码
  • ensurePrestart()方法开启线程执行
// ThreadPoolExecutor#ensurePrestart
void ensurePrestart() {int wc = workerCountOf(ctl.get());// worker数目小于corePoolSize,则添加一个worker。if (wc < corePoolSize)// 第二个参数 true 表示采用核心线程数量限制,false 表示采用 maximumPoolSizeaddWorker(null, true);// corePoolSize = 0的情况,至少开启一个线程,【担保机制】else if (wc == 0)addWorker(null, false);
}
复制代码

addWorker()方法实际上父类ThreadPoolExecutor的方法,这个方法在该文章 Java线程池源码深度解析中详细介绍过,这边做个总结:

  • 如果线程池中工作线程数量小于最大线程数,创建工作线程,执行任务。
  • 如果线程池重工作线程数量大于最大线程数,直接返回。

获取延迟任务take()原理

目前工作线程已经创建好了,工作线程开始工作了,它会从阻塞队列中获取延迟任务执行,这部分也是线程池里面的原理,不做展开,那我们看下它是如何实现延迟执行的? 主要关注如何从阻塞队列中获取任务。

  1. DelayedWorkQueue#take()方法获取延迟任务
  • 该方法会在上面的addWoker()方法创建工作线程后,工作线程中循环持续调用workQueue.take()方法获取延迟任务。
  • 该方法主要获取延迟队列中任务延迟时间小于等于0 的任务。
  • 如果延迟时间不小于0,那么调用条件队列的awaitNanos(delay)阻塞方法等待一段时间,等时间到了,延迟时间自然小于等于0了。
  • 获取到任务后,工作线程就可以开始执行调度任务了。
// DelayedWorkQueue#take()
public RunnableScheduledFuture<?> take() throws InterruptedException {final ReentrantLock lock = this.lock;// 加可中断锁lock.lockInterruptibly();try {// 自旋for (;;) {// 获取阻塞队列中的头结点RunnableScheduledFuture<?> first = queue[0];// 如果阻塞队列没有数据,为空if (first == null)// 等待队列不空,直至有任务通过 offer 入队并唤醒available.await();else {// 获取头节点的的任务还剩余多少时间才执行long delay = first.getDelay(NANOSECONDS);if (delay <= 0)// 到达触发时间,获取头节点并调整堆,重新选择延迟时间最小的节点放入头部return finishPoll(first);// 逻辑到这说明头节点的延迟时间还没到first = null;// 说明有 leader 线程在等待获取头节点,当前线程直接去阻塞等待if (leader != null)// 当前线程阻塞available.await();else {// 没有 leader 线程,【当前线程作为leader线程,并设置头结点的延迟时间作为阻塞时间】Thread thisThread = Thread.currentThread();leader = thisThread;try {// 当前线程通过awaitNanos方法等待delay时间后,会自动唤醒,往后面继续执行available.awaitNanos(delay);// 到达阻塞时间时,当前线程会从这里醒来,进入下一轮循环,就有可能执行了} finally {// t堆顶更新,leader 置为 null,offer 方法释放锁后,// 有其它线程通过 take/poll 拿到锁,读到 leader == null,然后将自身更新为leader。if (leader == thisThread)// leader 置为 null 用以接下来判断是否需要唤醒后继线程leader = null;}}}}} finally {// 没有 leader 线程并且头结点不为 null,唤醒阻塞获取头节点的线程,// 【如果没有这一步,就会出现有了需要执行的任务,但是没有线程去执行】if (leader == null && queue[0] != null)available.signal();// 解锁lock.unlock();}
}
复制代码
  1. finishPoll()方法获取到任务后执行

该方法主要做两个事情, 获取头节点并调整堆,重新选择延迟时间最小的节点放入头部。

private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {// 获取尾索引int s = --size;// 获取尾节点RunnableScheduledFuture<?> x = queue[s];// 将堆结构最后一个节点占用的 slot 设置为 null,因为该节点要尝试升级成堆顶,会根据特性下调queue[s] = null;// s == 0 说明 当前堆结构只有堆顶一个节点,此时不需要做任何的事情if (s != 0)// 从索引处 0 开始向下调整siftDown(0, x);// 出队的元素索引设置为 -1setIndex(f, -1);return f;
}
复制代码

延迟任务运行的原理

从延迟队列中获取任务后,工作线程会调用延迟任务的run()方法执行任务。

  1. ScheduledFutureTask#run()方法运行任务
  • 调用isPeriodic()方法判断任务是否是周期性任务还是非周期性任务
  • 如果任务是非周期任务,就调用父类的FutureTask#run()执行一次
  • 如果任务是非周期任务,就调用父类的FutureTask#runAndReset(), 返回true会设置下一次的执行时间,重新放入线程池的阻塞队列中,等待下次获取执行
public void run() {// 是否周期性,就是判断 period 是否为 0boolean periodic = isPeriodic();// 根据是否是周期任务检查当前状态能否执行任务,不能执行就取消任务if (!canRunInCurrentRunState(periodic))cancel(false);// 非周期任务,直接调用 FutureTask#run 执行一次else if (!periodic)ScheduledFutureTask.super.run();// 周期任务的执行,返回 true 表示执行成功else if (ScheduledFutureTask.super.runAndReset()) {// 设置周期任务的下一次执行时间setNextRunTime();// 任务的下一次执行安排,如果当前线程池状态可以执行周期任务,加入队列,并开启新线程reExecutePeriodic(outerTask);}
}
复制代码
  1. FutureTask#runAndReset()执行周期性任务
  • 周期任务正常完成后任务的状态不会变化,依旧是 NEW,不会设置 outcome 属性。
  • 但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中。
  • 方法返回 false,后续的该任务将不会再周期的执行
protected boolean runAndReset() {// 任务不是新建的状态了,或者被别的线程执行了,直接返回 falseif (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))return false;boolean ran = false;int s = state;try {Callable<V> c = callable;if (c != null && s == NEW) {try {// 执行方法,没有返回值c.call();ran = true;} catch (Throwable ex) {// 出现异常,把任务设置为异常状态,唤醒所有的 get 阻塞线程setException(ex);}}} finally {// 执行完成把执行线程引用置为 nullrunner = null;s = state;// 如果线程被中断进行中断处理if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}// 如果正常执行,返回 true,并且任务状态没有被取消return ran && s == NEW;
}
复制代码
  1. ScheduledFutureTask#setNextRunTime()设置下次执行时间
  • 如果属性period大于0,表示fixed-rate模式,直接加上period时间即可。
  • 如果属性period小于等于0, 表示是fixed-delay模式, 调用triggerTime重新计算下次时间。
// 任务下一次的触发时间
private void setNextRunTime() {long p = period;if (p > 0)// fixed-rate 模式,【时间设置为上一次执行任务的时间 + p】,两次任务执行的时间差time += p;else// fixed-delay 模式,下一次执行时间是【当前这次任务结束的时间(就是现在) + delay 值】time = triggerTime(-p);
}
复制代码
  1. ScheduledFutureTask#reExecutePeriodic(),重新放入阻塞任务队列,等待获取,进行下一轮执行
// ScheduledThreadPoolExecutor#reExecutePeriodic
void reExecutePeriodic(RunnableScheduledFuture<?> task) {if (canRunInCurrentRunState(true)) {// 【放入任务队列】super.getQueue().add(task);// 如果提交完任务之后,线程池状态变为了 shutdown 状态,需要再次检查是否可以执行,// 如果不能执行且任务还在队列中未被取走,则取消任务if (!canRunInCurrentRunState(true) && remove(task))task.cancel(false);else// 当前线程池状态可以执行周期任务,加入队列,并【根据线程数量是否大于核心线程数确定是否开启新线程】ensurePrestart();}
}
复制代码

结束语

本文讲解了ScheduledThreadPoolExecutor执行的实现原理,如果对大家帮助的话,留下一个赞。

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

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

相关文章

docker快速安装redis

一.背景 开发环境中&#xff0c;经常需要redis本地环境&#xff0c;方便开发。准备在本机的虚拟机里面准备一个redis环境。 二.版本信息 操作系统&#xff1a;Windows 10 家庭版 Oracle VM VirtualBox&#xff1a;版本 6.0.10 r132072 (Qt5.6.2) Ubuntu:16.04.6-desktop-a…

STM32CubeMX学习笔记(44)——USB接口使用(HID按键)

一、USB简介 USB&#xff08;Universal Serial BUS&#xff09;通用串行总线&#xff0c;是一个外部总线标准&#xff0c;用于规范电脑与外部设备的连接和通讯。是应用在 PC 领域的接口技术。USB 接口支持设备的即插即用和热插拔功能。USB 是在 1994 年底由英特尔、康柏、IBM、…

淘宝十年技术思考与总结,让人惊叹的进化脱变,最终确认版已发布

看了淘宝在将近10年时间里技术的革新&#xff0c;我对技术与业务有了更近一步的认识。 任何技术都是从小做起&#xff0c;一步步做起来的。如果你让04年的淘宝去做一个能承受10亿次访问的网站&#xff0c;马云那时候肯定会伤透脑筋&#xff0c;即使做半年都做不出来。但现在&a…

Java实现邮件发送

这里我们以QQ邮箱为例。 一、导入依赖:<dependencies><!-- https://mvnrepository.com/artifact/javax.activation/activation --><dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><versio…

联邦学习:联邦异构知识图谱划分

在联邦场景下,C个知识图谱位于不同的客户端上。知识图谱拥的实体集合之间可能会存在重叠,而其关系集合和元组集合之间则不会重叠。我们联系一下现实场景看这是合理的,比如在不同客户端对应不同银行的情况下,由于不同银行都有着自己的业务流程,所以关系集合不重叠。本文我们…

如何给PDF文件添加水印?PDF免费添加水印教程来了

有时候&#xff0c;为了不让别人盗用我们PDF文件里面的内容或图片&#xff0c;或者是出于宣传产品的目的&#xff0c;我们经常会需要给自己的PDF文件添加各种类型的水印&#xff0c;那你们知道如何给PDF文件添加水印吗&#xff1f;下面我们就来看看如何给PDF文件添加水印&#…

瞄准五金行业采购痛难点,智慧采购管理系统实现业务流程数据化,提高采购效率

五金行业采购一直是传统企业采购的软肋和头痛环节&#xff0c;无论从人力成本&#xff0c;物料成本&#xff0c;财务监管成本&#xff0c;物流成本等都存在一个整合服务需求&#xff0c;同时&#xff0c;传统五金行业采购难的问题&#xff0c;也一直制约着行业发展&#xff0c;…

股指期货高手陈(股指期货第一人)

​ 什么是股指期货&#xff0c;怎么玩&#xff1f;请教高手&#xff01; 股指期货&#xff08;Stock Index Futures&#xff0c;即股票价格指数期货&#xff0c;也可称为股价指数期货&#xff09;&#xff0c;是指以股价指数为标的资产的标准化期货合约。双方约定在未来某个特…

RK3399应用开发 | 移植libdrm到rk3399开发板(2.4.113)

一、下载源码 下载地址:https://dri.freedesktop.org/libdrm/。 这里我下载最新的2.4.113版本: wget https://dri.freedesktop.org/libdrm/libdrm-2.4.113.tar.xz解压: xz -d libdrm-2.4.113.tar.xz tar -xf libdrm-2.4.113.tar二、编译环境安装 1. 更新python ubuntu安…

【安信可NB-IoT模组EC系列应用笔记⑧】用NB-IoT模组EC系列了解LwM2M协议并接入云平台

文章目录前言一、测试准备1、硬件准备2、云平台准备二、云平台连接1.注册入网2.读取IMSI及IMEI3.利用IMSI及IMEI创建设备4.LwM2M连接云平台设备三、 数据互交1.ATMIPLNOTIFY 通知属性变化2.ATMIPLREADRSP 返回读取结果3.ATMIPLWRITERSP 发送写入结果4.ATMIPLEXECUTERSP 发送执行…

半乳糖-人血清白蛋白 Gal-HSA,Gal-PEG-HSA 半乳糖修饰人血清白蛋白

产品名称&#xff1a;半乳糖修饰人血清白蛋白 Gal-HSA 用途&#xff1a;科研 状态&#xff1a;固体/粉末/溶液 产品规格&#xff1a;1g/5g/10g 保存&#xff1a;冷藏 储藏条件&#xff1a;-20℃ 储存时间&#xff1a;1年 温馨提醒&#xff1a;仅供科研&#xff0c;不能用于人体…

supervisor管理prometheus进程

一、supervisor简单介绍 二、supervisor安装 三、supervisor部署应用 四、supervisorctl常用指令 五、supervisor测试 一、supervisor简单介绍 1、 概述 supervisor是一个Python编写的进程管理工具&#xff0c;可以方便启动、重启、关闭、单个或多个进程&#xff0c;可以简…

中国数字视听行业全景调研与投资趋势预测报告

数字视听和视听技术的概念 传统视听技术既包括视觉技术&#xff0c;也包括听觉技术&#xff0c;即模拟信号&#xff0c;包括录音录像、摄影等。随着时代和科技的发展&#xff0c;先进的计算机技术在检察系统和办公业务中得到广泛应用&#xff0c;传统的视听技术也逐渐向数字化方…

硬件开发趋势与技术探索

LiveVideoStackCon 2022 音视频技术大会 北京站将于11月25日至26日在北京丽亭华苑酒店召开&#xff0c;本次大会将延续【音视频无限可能】的主题&#xff0c;邀请业内众多企业及专家学者&#xff0c;将他们在过去一年乃至更长时间里对音视频在更多领域和场景下应用的探索、在实…

CSS3 1 CSS3 响应式布局 1.3 Grid 布局

CSS3 文章目录CSS31 CSS3 响应式布局1.3 Grid 布局1.3.1 Grid 布局简介1.3.2 开启grid 布局1.3.3 排列元素1.3.4 对齐方式1 CSS3 响应式布局 1.3 Grid 布局 【【迄今为止最易懂】2分钟掌握 CSS Grid 布局】 https://www.bilibili.com/video/BV18p411A7JB?share_sourcecopy_w…

YOLOv5、v7改进之三十八:引入RepVGG模型结构

前 言&#xff1a;作为当前先进的深度学习目标检测算法YOLOv7&#xff0c;已经集合了大量的trick&#xff0c;但是还是有提高和改进的空间&#xff0c;针对具体应用场景下的检测难点&#xff0c;可以不同的改进方法。此后的系列文章&#xff0c;将重点对YOLOv7的如何改进进行详…

【微服务】微服务万字实战,带你了解工程原理

微服务实战1、前期准备1.1 技术选型1.2 模块设计1.3 微服务调用2、创建父工程3、创建基础模块3.1 导入依赖3.2 创建实体类4、创建用户微服务4.1 创建shop-user模块4.2 用户微服务启动类4.3 创建配置文件5、创建商品微服务5.1 创建shop_product模块5.2 商品微服务启动类5.3 创建…

【自学CSS笔记第7篇】——CSS三大特征(这一篇就够了)

其实&#xff0c;我清楚的知道什么是对的什么是错的&#xff0c;什么该做什么不该做&#xff0c;然而懒惰的天性驱使我们每每做出错误的决定&#xff0c;结束后我又再一次重复着厌倦和懊恼。 目录 CSS的三大特性总览: 层叠性&#xff1a; 继承性&#xff1a; 优先级&#x…

链路状态路由协议 OSPF (二)

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.Router ID 1.什么是Router ID 2.获得Router ID方法 二.DR和…

Libevent库的学习

目录 Libevent 概述 Libevent 使用模型 使用Libevent的基本流程&#xff1a; libevent 的核心&#xff0c;event 事件 1. 创建一个事件event 2. 释放event_free 3. 注册event 4. 信号事件 5. 销毁event_base Libevent 结构图 使用libevent库去实现tcp服务器 Libev…