并发编程之定时任务定时线程池

news2024/12/22 20:39:53

ScheduledThreadPoolExecutor

定时线程池类的类结构图

它用来处理延时任务或定时任务。

 

它接收SchduledFutureTask类型的任务,是线程池调度任务的最小单位,有三种提交任务的方式:

  1. schedule
  2. scheduledAtFixedRate
  3. scheduledWithFixedDelay

它采用DelayQueue存储等待的任务

  1. DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若

time相同则根据sequenceNumber排序;

  1. DelayQueue也是一个无界队列;

SchduledFutureTask

SchduledFutureTask接收的参数(成员变量):

  1. private long time:任务开始的时间
  2. private final long sequenceNumber;:任务的序号
  3. private final long period:任务执行的时间间隔

工作线程的执行过程:

1、工作线程会从DelayQueue取已经到期的任务去执行;

2、执行结束后重新设置任务的到期时间,再次放回DelayQueue

ScheduledThreadPoolExecutor会把待执行的任务放到工作队列DelayQueue中,

DelayQueue封装了一个PriorityQueue,PriorityQueue会对队列中的

ScheduledFutureTask进行排序,具体的排序算法实现如下:

  1. public int compareTo(Delayed other) {
  2. if (other == this) // compare zero if same object
  3. return 0;
  4. if (other instanceof ScheduledFutureTask) {
  5. ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
  6. long diff = time x.time;
  7. if (diff < 0)
  8. return 1;
  9. else if (diff > 0)
  10. return 1;
  1. else if (sequenceNumber < x.sequenceNumber)
  2. return 1;
  3. else
  4. return 1;

15   }

16   long diff = getDelay(NANOSECONDS) other.getDelay(NANOSECONDS);

17   return (diff < 0) ? ‐1 : (diff > 0) ? 1 : 0;

18 }


  1. 首先按照time排序,time小的排在前面,time大的排在后面;
  2. 如果time相同,按照sequenceNumber排序,sequenceNumber小的排在前  面,sequenceNumber大的排在后面,换句话说,如果两个task的执行时间相同, 优先执行先提交的task。

SchduledFutureTask之run方法实现

run方法是调度task的核心,task的执行实际上是run方法的执行。

  1. public void run() {
  2. boolean periodic = isPeriodic();
  3. //如果当前线程池已经不支持执行任务,则取消
  4. if (!canRunInCurrentRunState(periodic))
  5. cancel(false);
  6. //如果不需要周期性执行,则直接执行run方法然后结束
  7. else if (!periodic)
  8. ScheduledFutureTask.super.run();
  9. //如果需要周期执行,则在执行完任务以后,设置下一次执行时间
  10. else if (ScheduledFutureTask.super.runAndReset()) {
  11. // 计算下次执行该任务的时间
  12. setNextRunTime();
  13. //重复执行任务
  14. reExecutePeriodic(outerTask);

15   }

16 }

  1. 如果当前线程池运行状态不可以执行任务,取消该任务,然后直接返回,否则执行

步骤2;

  1. 如果不是周期性任务,调用FutureTask中的run方法执行,会设置执行结果,然后直接返回,否则执行步骤3;
  2. 如果是周期性任务,调用FutureTask中的runAndReset方法执行,不会设置执行结果,然后直接返回,否则执行步骤4和步骤5;
  3. 计算下次执行该任务的具体时间;
  4. 重复执行任务。

  1. void reExecutePeriodic(RunnableScheduledFuture<?> task) {
  2. if (canRunInCurrentRunState(true)) {
  3. super.getQueue().add(task);
  4. if (!canRunInCurrentRunState(true) && remove(task))
  5. task.cancel(false);
  6. else
  7. ensurePrestart();

8   }

9 }

reExecutePeriodic方法

该方法和delayedExecute方法类似,不同的是:

  1. 由于调用reExecutePeriodic方法时已经执行过一次周期性任务了,所以不会

reject当前任务;

  1. 传入的任务一定是周期性任务。

线程池任务的提交

首先是schedule方法,该方法是指任务在指定延迟时间到达后触发,只会执行一次。

  1. public ScheduledFuture<?> schedule(Runnable command,
  2. long delay,
  3. TimeUnit unit) {
  4. //参数校验
  5. if (command == null || unit == null)
  6. throw new NullPointerException();
  7. //这里是一个嵌套结构,首先把用户提交的任务包装成ScheduledFutureTask
  8. //然后在调用decorateTask进行包装,该方法是留给用户去扩展的,默认是个空方法
  9. RunnableScheduledFuture<?> t = decorateTask(command,
  10. new ScheduledFutureTask<Void>(command, null,
  1. triggerTime(delay, unit)));
  2. //包装好任务以后,就进行提交了
  3. delayedExecute(t);
  4. return t;

15 }

  1. private void delayedExecute(RunnableScheduledFuture<?> task) {
  2. //如果线程池已经关闭,则使用拒绝策略把提交任务拒绝掉
  3. if (isShutdown())
  4. reject(task);
  5. else {
  6. //ThreadPoolExecutor不同,这里直接把任务加入延迟队列
  7. super.getQueue().add(task);//使用用的DelayedWorkQueue
  8. //如果当前状态无法执行任务,则取消
  9. if (isShutdown() &&
  10. !canRunInCurrentRunState(task.isPeriodic()) &&
  11. remove(task))
  12. task.cancel(false);
  13. else
  14. //这里是增加一个worker线程,避免提交的任务没有worker去执行
  15. //原因就是该类没有像ThreadPoolExecutor一样,woker满了才放入队列
  16. ensurePrestart();

17   }

18 }


任务提交方法:

DelayedWorkQueue

ScheduledThreadPoolExecutor之所以要自己实现阻塞的工作队列,是因为

ScheduledThreadPoolExecutor要求的工作队列有些特殊。

DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和

PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以

DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面(注意:这里的顺序并不是绝对的,堆中的排序只保证了子节点的下次执行时间要比父节点的下次执行时间要大,而叶子节点之间并不一定是顺序的,下文中会说明)。

堆结构如下图:

 

可见,DelayedWorkQueue是一个基于最小堆结构的队列。堆结构可以使用数组表示,可以转换成如下的数组:

在这种结构中,可以发现有如下特性:

假设,索引值从0开始,子节点的索引值为k,父节点的索引值为p,则:

  1. 一个节点的左子节点的索引为:k = p * 2 + 1;
  2. 一个节点的右子节点的索引为:k = (p + 1) * 2;
  3. 一个节点的父节点的索引为:p = (k - 1) / 2。

为什么要使用DelayedWorkQueue呢?

定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。

DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。

DelayedWorkQueue属性

  1. // 队列初始容量
  2. private static final int INITIAL_CAPACITY = 16;
  3. // 根据初始容量创建RunnableScheduledFuture类型的数组
  4. private RunnableScheduledFuture<?>[] queue =
  5. new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
  6. private final ReentrantLock lock = new ReentrantLock();
  7. private int size = 0;
  8. // leader线程
  9. private Thread leader = null;
  10. // 当较新的任务在队列的头部可用时,或者新线程可能需要成为leader,则通过该条件发出信号
  11. private final Condition available = lock.newCondition();

注意这里的leader,它是Leader-Follower模式的变体,用于减少不必要的定时等待。什么意思呢?对于多线程的网络模型来说:

所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:

proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下 次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。

offer方法

  1. public boolean offer(Runnable x) {
  2. //参数校验
  3. if (x == null)
  4. throw new NullPointerException();
  5. RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
  6. final ReentrantLock lock = this.lock;
  7. lock.lock();
  8. try {
  9. //查看当前元素数量,如果大于队列长度则进行扩容
  1. int i = size;
  2. if (i >= queue.length)
  3. grow();
  4. //元素数量加1
  5. size = i + 1;
  6. //如果当前队列还没有元素,则直接加入头部

16   if (i == 0) {

  1. queue[0] = e;
  2. //记录索引
  3. setIndex(e, 0);
  4. } else {
  5. //把任务加入堆中,并调整堆结构,这里就会根据任务的触发时间排列
  6. //把需要最早执行的任务放在前面
  7. siftUp(i, e);

24   }

  1. //如果新加入的元素就是队列头,这里有两种情况
  2. //1.这是用户提交的第一个任务
  3. //2.新任务进行堆调整以后,排在队列头
  4. if (queue[0] == e) {
  5. // leader设置为null为了使在take方法中的线程在通过available.signal

();后会执行available.awaitNanos(delay);

  1. leader = null;
  2. //加入元素以后,唤醒worker线程
  3. available.signal();

33   }

  1. } finally {
  2. lock.unlock();

36   }

37   return true;

38 }

    1. void siftUp(int k, RunnableScheduledFuture<?> key) {
    1. // 找到父节点的索引
    2. while (k > 0) {
    3. // 获取父节点

    任务排序sift方法
  1. int parent = (k 1) >>> 1;
  2. RunnableScheduledFuture<?> e = queue[parent];
  3. // 如果key节点的执行时间大于父节点的执行时间,不需要再排序了
  4. if (key.compareTo(e) >= 0)
  5. break;
  6. // 如果key.compareTo(e) < 0,说明key节点的执行时间小于父节点的执时间,需要把父节点移到后面
  7. queue[k] = e;
  8. setIndex(e, k);
  9. // 设置索引为k
  10. k = parent;

15   }

  1. // key设置为排序后的位置中
  2. queue[k] = key;
  3. setIndex(key, k);

19 }

代码很好理解,就是循环的根据key节点与它的父节点来判断,如果key节点的执行时间小于父节点,则将两个节点交换,使执行时间靠前的节点排列在队列的前面。

假设新入队的节点的延迟时间(调用getDelay()方法获得)是5,执行过程如下:

  1. 先将新的节点添加到数组的尾部,这时新节点的索引k为7:

  1. 计算新父节点的索引:parent = (k - 1) >>> 1,parent = 3,那么queue[3]的时间间隔值为8,因为 5 < 8 ,将执行queue[7] = queue[3

  1. 这时将k设置为3,继续循环,再次计算parent为1,queue[1]的时间间隔为3, 因为 5 > 3 ,这时退出循环,最终k为3:

可见,每次新增节点时,只是根据父节点来判断,而不会影响兄弟节点。

take方法

  1. public RunnableScheduledFuture<?> take() throws InterruptedExcep tion {
  2. final ReentrantLock lock = this.lock;
  3. lock.lockInterruptibly();
  4. try {

5   for (;;) {

  1. RunnableScheduledFuture<?> first = queue[0];
  2. if (first == null)
  3. available.await();
  4. else {
  5. // 计算当前时间到执行时间的时间间隔
  6. long delay = first.getDelay(NANOSECONDS);
  7. if (delay <= 0)
  8. return finishPoll(first);
  9. first = null; // don't retain ref while waiting
  1. // leader不为空,阻塞线程
  2. if (leader != null)
  3. available.await();
  4. else {
  5. // leader为空,则把leader设置为当前线程,
  6. Thread thisThread = Thread.currentThread();
  7. leader = thisThread;
  8. try {
  9. // 阻塞到执行时间
  10. available.awaitNanos(delay);
  11. } finally {
  12. // 设置leader = null,让其他线程执行available.awaitNanos(delay);
  13. if (leader == thisThread)
  14. leader = null;

29   }

30   }

31   }

32   }

  1. } finally {
  2. // 如果leader不为空,则说明leader的线程正在执行available.awaitNano s(delay);
  3. // 如果queue[0] == null,说明队列为空
  4. if (leader == null && queue[0] != null)
  5. available.signal();
  6. lock.unlock();

39   }

40 }


take方法是什么时候调用的呢?在ThreadPoolExecutor中,介绍了getTask方法,工

作线程会循环地从workQueue中取任务。但定时任务却不同,因为如果一旦getTask方法 取出了任务就开始执行了,而这时可能还没有到执行的时间,所以在take方法中,要保证只有在到指定的执行时间的时候任务才可以被取走。

再来说一下leader的作用,这里的leader是为了减少不必要的定时等待,当一个线程成为leader时,它只等待下一个节点的时间间隔,但其它线程无限期等待。 leader线程必须在从take()或poll()返回之前signal其它线程,除非其他线程成为了leader。

举例来说,如果没有leader,那么在执行take时,都要执行

available.awaitNanos(delay),假设当前线程执行了该段代码,这时还没有signal,第二个线程也执行了该段代码,则第二个线程也要被阻塞。多个这时执行该段代码是没有作用的,因为只能有一个线程会从take中返回queue[0](因为有lock),其他线程这时再返回

for循环执行时取的queue[0],已经不是之前的queue[0]了,然后又要继续阻塞。

所以,为了不让多个线程频繁的做无用的定时等待,这里增加了leader,如果leader不为空,则说明队列中第一个节点已经在等待出队,这时其它的线程会一直阻塞,减少了无用的阻塞(注意,在finally中调用了signal()来唤醒一个线程,而不是signalAll())。

poll 方法

下面看下poll方法,与take类似,但这里要提供超时功能:

  1. public RunnableScheduledFuture<?> poll(long timeout, TimeUnit un it)
  2. throws InterruptedException {
  3. long nanos = unit.toNanos(timeout);
  4. final ReentrantLock lock = this.lock;
  5. lock.lockInterruptibly();
  6. try {

7   for (;;) {

  1. RunnableScheduledFuture<?> first = queue[0];
  2. if (first == null) {
  3. if (nanos <= 0)
  4. return null;
  5. else
  6. nanos = available.awaitNanos(nanos);
  7. } else {
  8. long delay = first.getDelay(NANOSECONDS);
  9. // 如果delay <= 0,说明已经到了任务执行的时间,返回。
  10. if (delay <= 0)
  11. return finishPoll(first);
  12. // 如果nanos <= 0,说明已经超时,返回null
  13. if (nanos <= 0)
  14. return null;
  15. first = null; // don't retain ref while waiting
  16. // nanos < delay 说明需要等待的时间小于任务要执行的延迟时间
  1. // leader != null 说明有其它线程正在对任务进行阻塞
  2. // 这时阻塞当前线程nanos纳秒
  3. if (nanos < delay || leader != null)
  4. nanos = available.awaitNanos(nanos);
  5. else {
  6. Thread thisThread = Thread.currentThread();
  7. leader = thisThread;
  8. try {
  9. // 这里的timeLeft表示delay减去实际的等待时间
  10. long timeLeft = available.awaitNanos(delay);
  11. // 计算剩余的等待时间
  12. nanos ‐= delay timeLeft;
  13. } finally {
  14. if (leader == thisThread)
  15. leader = null;

39   }

40   }

41   }

42   }

  1. } finally {
  2. if (leader == null && queue[0] != null)
  3. available.signal();
  4. lock.unlock();

47   }

48 }


finishPoll方法

当调用了take或者poll方法能够获取到任务时,会调用该方法进行返回:

  1. private RunnableScheduledFuture<?> finishPoll(RunnableScheduledF uture<?> f) {
  2. // 数组长度‐1
  3. int s = ‐‐size;
  4. // 取出最后一个节点
  5. RunnableScheduledFuture<?> x = queue[s];
  6. queue[s] = null;

7 // 长度不为0,则从第一个元素开始排序,目的是要把最后一个节点放到合适的位置上

8   if (s != 0)

  1. siftDown(0, x);
  2. setIndex(f, 1);
  3. return f;

12 }


siftDown方法

siftDown方法使堆从k开始向下调整:

  1. private void siftDown(int k, RunnableScheduledFuture<?> key) {
  2. // 根据二叉树的特性,数组长度除以2,表示取有子节点的索引
  3. int half = size >>> 1;
  4. // 判断索引为k的节点是否有子节点
  5. while (k < half) {
  6. // 左子节点的索引
  7. int child = (k << 1) + 1;
  8. RunnableScheduledFuture<?> c = queue[child];
  9. // 右子节点的索引
  10. int right = child + 1;
  11. // 如果有右子节点并且左子节点的时间间隔大于右子节点,取时间间隔最小的节点
  12. if (right < size && c.compareTo(queue[right]) > 0)
  13. c = queue[child = right];
  14. // 如果key的时间间隔小于等于c的时间间隔,跳出循环
  15. if (key.compareTo(c) <= 0)
  16. break;
  17. // 设置要移除索引的节点为其子节点
  18. queue[k] = c;
  19. setIndex(c, k);
  20. k = child;

21   }

  1. // key放入索引为k的位置
  2. queue[k] = key;
  3. setIndex(key, k);

25 }

siftDown方法执行时包含两种情况,一种是没有子节点,一种是有子节点(根据half判断)。例如:

没有子节点的情况: 假设初始的堆如下:

 

假设 k = 3 ,那么 k = half ,没有子节点,在执行siftDown方法时直接把索引为3的节点设置为数组的最后一个节点:

有子节点的情况:

假设 k = 0 ,那么执行以下步骤:

1、获取左子节点,child = 1 ,获取右子节点, right = 2 :

2、由于 right < size ,这时比较左子节点和右子节点时间间隔的大小,这里 3 < 7 ,所以 c = queue[child] ;

3、比较key的时间间隔是否小于c的时间间隔,这里不满足,继续执行,把索引为k的

节点设置为c,然后将k设置为child;

4、因为 half = 3 ,k = 1 ,继续执行循环,这时的索引变为:

5、这时再经过如上判断后,将k的值为3,最终的结果如下:

6、最后,如果在finishPoll方法中调用的话,会把索引为0的节点的索引设置为-1,表示已经删除了该节点,并且size也减了1,最后的结果如下:

可见,siftdown方法在执行完并不是有序的,但可以发现,子节点的下次执行时间一 定比父节点的下次执行时间要大,由于每次都会取左子节点和右子节点中下次执行时间最小的节点,所以还是可以保证在take和poll时出队是有序的。

remove方法

  1. public boolean remove(Object x) {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. int i = indexOf(x);

6   if (i < 0)

7   return false;

8

  1. setIndex(queue[i], 1);
  2. int s = ‐‐size;
  3. RunnableScheduledFuture<?> replacement = queue[s];
  4. queue[s] = null;

13   if (s != i) {

14   // i开始向下调整

  1. siftDown(i, replacement);
  2. // 如果queue[i] == replacement,说明i是叶子节点
  3. // 如果是这种情况,不能保证子节点的下次执行时间比父节点的大
  4. // 这时需要进行一次向上调整
  5. if (queue[i] == replacement)
  6. siftUp(i, replacement);

21   }

  1. return true;
  2. } finally {
  3. lock.unlock();

25   }

26 }


假设初始的堆结构如下

这时要删除8的节点,那么这时 k = 1,key为最后一个节点:

这时通过上文对siftDown方法的分析,siftDown方法执行后的结果如下

这时会发现,最后一个节点的值比父节点还要小,所以这里要执行一次siftUp方法来保证子节点的下次执行时间要比父节点的大,所以最终结果如下:

总结

主要总结为以下几个方面:

与Timer执行定时任务的比较,相比Timer,ScheduedThreadPoolExecutor有什么优点; ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以它也是一个线程池,也有coorPoolSize和workQueue,ScheduledThreadPoolExecutor特殊的地方在于,自己实现了优先工作队列DelayedWorkQueue;

ScheduedThreadPoolExecutor实现了ScheduledExecutorService,所以就有了任务调度的方法,如schedule,scheduleAtFixedRate和

scheduleWithFixedDelay,同时注意他们之间的区别;

内部类ScheduledFutureTask继承自FutureTask,实现了任务的异步执行并且可以获取返回结果。同时也实现了Delayed接口,可以通过getDelay方法获取将要执行的时间间隔;

周期任务的执行其实是调用了FutureTask类中的runAndReset方法,每次执行完不设置结果和状态。

详细分析了DelayedWorkQueue的数据结构,它是一个基于最小堆结构的优先队列,并且每次出队时能够保证取出的任务是当前队列中下次执行时间最小的任务。同时注意一下优先队列中堆的顺序,堆中的顺序并不是绝对的,但要保证子节点的值要比父节点的值要大,这样就不会影响出队的顺序。

总体来说,ScheduedThreadPoolExecutor的重点是要理解下次执行时间的计算,以及优先队列的出队、入队和删除的过程,这两个是理解ScheduedThreadPoolExecutor的关 键。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Numpy函数详解

目录 有关矩阵行列交换问题 法一 法二 行进行互换 列进行互换 insert函数 delete函数 append函数 where函数 默认第一个为行索引第二个为列索引。 行列索引都是以作为起始数值作为第一列或者第一行。(根据c数组的知识进行理解) 有关矩阵行列交换问题 法一 使用矩阵…

消灭空指针,Java 8 给我们更好的解决方案

前言 大家好&#xff0c;我是小郭。 在平时的业务开发中&#xff0c;空指针是我们经常遇到的问题&#xff0c; 他可能会导致我们的流程无法正常进行或者一些意外情况的发生。 这就是我们需要避免空指针的原因&#xff0c;那我们有哪些方式去解决这个问题呢&#xff1f; 空…

英语语法基础

英语语法知识点1   第一点&#xff1a;动词的变化   1、代词及be动词   主格 I we you you she/he/it they   宾格 me us you you her/him/it them   代词所有格 my our your your her/his/its their   名词性代词 mine ours yours yours hers/his/its theirs   …

FPGA数字信号、图像

1、基于FPGA的数字图像处理原理及应用 (牟新刚) 本书首先介绍FPGA程序设计和图像与视频处理的关键基础理论&#xff0c;然后通过实例代码详细讲解了如何利用FPGA实现直方图操作中的直方图统计/均衡化/线性拉伸/规定化、线性滤波器操作中的均值滤波器、Sobel算子(滤波、求模、求…

Redis从理论到实战:用Redis解决缓存穿透、缓存击穿问题(提供解决方案)

文章目录一、缓存穿透1、什么是缓存穿透2、解决方案二、缓存雪崩三、缓存击穿1、什么是缓存击穿2、解决方案3、互斥锁解决缓存击穿问题4、逻辑删除解决缓存击穿问题加油加油&#xff0c;不要过度焦虑(*^▽^*) 一、缓存穿透 1、什么是缓存穿透 缓存穿透是指客户端请求的数据在…

重组蛋白/细胞因子的实验操作

在我们进行抗体制备、ELISA、药物研究、免疫实验、细胞培养、晶体结构分析等实验时&#xff0c;免不了要和重组蛋白打交道。MCE 重组蛋白产品涵盖超过 2000 种不同功能的重组蛋白&#xff0c;具有批次间一致性&#xff0c;优异的活性以及极低的内毒素水平等特性&#xff0c;可用…

小学生python游戏编程arcade----坦克大战3

小学生python游戏编程arcade----坦克大战3前言整体解绍1、坦克大战3--未完&#xff0c;只是功能初具1.1 文件结构1.2 类1.3 角色类1.4 粒子类1.5 主程序框1.6 main函数1.7 效果图1.8 代码实现源码获取前言 接以上多篇文章解绍arcade游戏编程的基本知识&#xff0c;回归主题&am…

数商云B2B电商系统商品管理功能剖析,助力家用电器企业业务提效

如今&#xff0c;传统家用电器企业的发展空间不断受到电商渠道的积压&#xff0c;由于许多家电企业缺乏数字化的管理工具&#xff0c;导致管理低效&#xff0c;还很容易产生存货积压、供不应求等问题。随着家用电器市场需求疲软、竞争日趋白热化&#xff0c;家用电器企业亟须加…

密码学 数字签名

消息鉴别的缺陷 消息鉴别保证了数据完整性&#xff0c;消息不被第三方侵犯&#xff0c;但是不保证双方之间的欺骗。如果A发送认证消息给B&#xff0c;可能会存在多种争议&#xff1a; B伪造一个不同的消息&#xff0c;声称是A发的 A否认发过这个消息&#xff0c;B无法证明A确实…

爬虫软件是什么意思

爬虫软件的正宗名称是python计算机编程语言&#xff0c;广泛应用于系统管理任务的处理和Web编程。 python软件为什么叫爬虫软件&#xff1f;爬虫通常指的是网络爬虫&#xff0c;就是一种按照一定的规则&#xff0c;自动地抓取万维网信息的程序或者脚本。所以Python被很多人称为…

抗疫众志成城网页设计成品 抗击疫情感动人物网页制作模板 大学生抗疫静态HTML网页源码 dreamweaver网页作业致敬逆行者网页设计作品

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

缓存过期都有哪些策略?

常见以下四种缓存过期策略&#xff1a; 定时过期&#xff1a;每个设置过期时间的key都需要创建⼀个定时器&#xff0c;到过期时间就会立即清除。该策略可以⽴ 即清除过期的数据&#xff0c;对内存很友好&#xff1b;但是会占⽤⼤量的CPU资源去处理过期的数据&#xff0c;从⽽影…

C++11、17、20的内存管理-指针、智能指针和内存池从基础到实战(上)

C11、17、20的内存管理-指针、智能指针和内存池从基础到实战&#xff08;上&#xff09;第一章 指针原理和快速入门1、第一个指针程序-详解指针代码2、图示进程的内存空间划分分析代码区_堆栈_内核空间3、各种内存空间-堆_栈_全局地址代码演示4、图解堆栈空间分配对应的指针代码…

中学化学教学参考杂志社中学化学教学参考编辑部2022年第12期目录

教学论坛《中学化学教学参考》投稿&#xff1a;cn7kantougao163.com 探索有效问题的层次化设计和结构化布局 于滨; 1-5 “双减”政策下初中化学作业设计策略与方法探究 王洁; 5-7 中学化学课程思政教学案例设计 兰青;靳素娟;马玲;谢海泉; 8-9 化学教学情境创设…

5G无线技术基础自学系列 | 基础参数及帧结构

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 5G在空中接口的参数定义大多和LTE一致&…

Centos7 安装Seata1.5.1

一、环境说明 IP操作系统程序备注10.0.61.22centos7.9PostgreSQL-14.11已提前部署10.0.61.21centos7.9Nacos-2.1.0已提前部署10.0.61.22centos7.9seata-server-1.5.1本文将要部署 二、部署 1. 下载 wget https://github.com/seata/seata/releases/download/v1.5.1/seata-ser…

【Java八股文总结】之Spring MVC

文章目录Spring MVC1、Spring MVC介绍2、Spring MVC的核心组件3、Spring MVC工作流程4、Spring MVC Restful风格的接口的流程&#xff1f;5、Spring MVC请求参数的种类1. 请求参数&#xff08;传递json数据&#xff09;2. 日期类型参数传递6、Spring MVC开发中用到的工具7、Spr…

SRM采购管理系统投标管理模块:阳光招采,助力建筑材料企业智慧采购

在建筑行业企业材料管理的四大业务环节即采购、运输、储备和供应&#xff0c;采购是首要环节&#xff0c;没有采购&#xff0c;就没有材料供应&#xff0c;就没有施工生产的顺利进行&#xff0c;因此采购是决定其他三项业务环节的基础因素。 随着流通环节的不断发展壮大&#…

[go学习笔记.第十六章.TCP编程] 1.基本介绍以及入门案例

1.基本介绍 Golang 的主要设计目标之一就是面向大规模后端服务程序&#xff0c;网络通信这块是服务端程序必不可少也是至关重要的一部分. 网络编程有两种户 (1).TCP Socket 编程&#xff0c;是网络编程的主流。之所以叫TCP Socket 编程&#xff0c;是因为底层是基于 TCP/IP 协议…

镍离子去除专业吸附技术,深度除镍工程段工艺设计

含镍废水具有较大的复杂性&#xff0c;难以利用单一的处理方法进行有效处理&#xff0c;现多采用综合处理技术来实现其达标排放及资源的综合利用。 现有的含镍废水处理技术可分为传统化学法、物理法以及电化学法三类。 传统的化学法包括化学沉淀法以及絮凝法等&#xff0c;是通…