ScheduledThreadPoolExecutor

在进一步了解ScheduledThreadPoolExecutor类之前,先学习下ScheduledFutureTask类的构造。

1. ScheduledFutureTask

类的继承图:

类图显示,该类实现Runnable接口并继承FutureTask类,表明该类是一个被包装的任务,同时该类实现Delayed接口和Comparable< Delayed>接口,表明该任务具有延迟性和优先级。所以,我们可以初步断定ScheduledFutureTask任务使得Runnable任务具有延迟性。下面详细分析该类。

1.1 ScheduledFutureTask类的五个属性和三个构造函数
// FIFO队列的序列号
private final long sequenceNumber;
// 以毫秒为单位的相对于任务创建时刻的等待时间,即延迟时间
private long time;
// 以纳秒为单位的周期时间,正数表明fixed-rate执行,负数表明delay-rate执行,0表明非重复
private final long period;
// 当前任务
RunnableScheduledFuture<V> outerTask = this;
// 进入延迟队列的索引值,它便于取消任务
int heapIndex;

类的三个构造函数:

// 创建延迟时间为ns的非重复任务,返回结果为result
ScheduledFutureTask(Runnable r, V result, long ns) {super(r, result);this.time = ns;this.period = 0;  // 0表示非重复this.sequenceNumber = sequencer.getAndIncrement(); // 当前任务的序列号
}// 创建延迟时间为ns,周期时间为period,返回结果为result的任务
ScheduledFutureTask(Runnable r, V result, long ns, long period) {super(r, result);this.time = ns;this.period = period;this.sequenceNumber = sequencer.getAndIncrement();
}
// 创建延迟时间为ns的非重复任务
ScheduledFutureTask(Callable<V> callable, long ns) {super(callable);this.time = ns;this.period = 0;this.sequenceNumber = sequencer.getAndIncrement();
}

通过类的属性和构造函数可知,ScheduledFutureTask类使得Runnable任务可以延迟执行,甚至设置周期执行;同时记录每个Runnable任务进入延迟队列的序列号.

1.2 ScheduledFutureTask的run()方法

ScheduledFutureTask类继承FutureTask类,所以可以执行带有返回结果的任务.

// 重写FutureTask的run方法
public void run() {// 属性period不等于0返回true,表明周期任务;否则返回false,表明非周期任务boolean periodic = isPeriodic(); // 当前线程池状态下是否可以继续执行if (!canRunInCurrentRunState(periodic))cancel(false); // 终止线程并移除任务// 非周期任务执行FutureTask的run()方法else if (!periodic)ScheduledFutureTask.super.run();// 周期任务,新建任务且可执行else if (ScheduledFutureTask.super.runAndReset()) {// 根据period值设置该任务的time值,如果period为正数,则直接与time值相加,如果是负数,则去掉符号后与系统当前时间相加setNextRunTime();// 当前任务加入队列reExecutePeriodic(outerTask);}
}
private void setNextRunTime() {// 构造函数中设置的周期时间long p = period;// 若周期时间为正数if (p > 0)// 则把周期时间与延迟时间相加作为任务下次执行的时间time += p;else// 若为负数,则忽略延迟时间,以当前时间为基准加上周期时间作为下次任务执行时间time = triggerTime(-p);
}
long triggerTime(long delay) {return now() +((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

如果delay值小于Long.MAX_VALUE值的一半,则取delay的值,否则delay值取overflowFree(delay)方法返回的值.

private long overflowFree(long delay) {// 获取队列的头部节点,即优先级最高的任务Delayed head = (Delayed) super.getQueue().peek();if (head != null) {// 获取任务的延迟时间long headDelay = head.getDelay(NANOSECONDS);// 若任务时间已到但还没有被处理且delay和headDelay相加溢出if (headDelay < 0 && (delay - headDelay < 0))// delay取差值delay = Long.MAX_VALUE + headDelay;}return delay;
}

overflowFree()方法的目的是保证队列内任务的延迟时间都在Long.MAX_VALUE范围内

run()在处理任务时,会根据任务是否是周期任务走不通的流程:

  • 非周期任务,则采用futureTask类的run()方法,不存储优先队列;
  • 周期任务,首先确定任务的延迟时间,然后把延迟任务插入优先队列;
1.3 ScheduledFutureTask的reExecutePeriodic(outerTask)方法

该方法是把周期任务插入优先队列的过程.

void reExecutePeriodic(RunnableScheduledFuture<?> task) {// 当前状态可以运行线程if (canRunInCurrentRunState(true)) {// task任务放入delayedWorkQueue队列中,实际执行offer()方法super.getQueue().add(task);// 当前状态(shutdown)不可以运行线程,删除任务 if (!canRunInCurrentRunState(true) && remove(task))task.cancel(false);  // 且中断线程elseensurePrestart();  // 否则判断是否要创建新的线程}
}
boolean canRunInCurrentRunState(boolean periodic) {return isRunningOrShutdown(periodic ?continueExistingPeriodicTasksAfterShutdown :executeExistingDelayedTasksAfterShutdown);
}

continueExistingPeriodicTasksAfterShutdown属性值默认为false,表示shutDown状态下取消周期性任务.
executeExistingDelayedTasksAfterShutdown属性值默认为true,表示shutDown状态下不会取消非周期性任务.
我们这里判断的是周期性任务,所以取continueExistingPeriodicTasksAfterShutdown属性的值.

void ensurePrestart() {int wc = workerCountOf(ctl.get());if (wc < corePoolSize)addWorker(null, true);else if (wc == 0)addWorker(null, false);
}

方法表明即使corePoolSize为0,也会创建一个线程.该线程会去等待获取队列中的任务.

通过上面的分析,可知reExecutePeriodic(outerTask)方法把周期任务插入优先队列的过程:

  • 判断当前状态是否可以插入任务;
  • 任务插入到优先队列;
  • 创建新的线程;
1.4 ScheduledFutureTask的compareTo(T)方法

compareTo(T)方法是核心方法,在周期任务插入优先队列的时候会根据该方法判断队列的插入位置.

public int compareTo(Delayed other) {// 如果与自己比较则返回0if (other == this) // compare zero if same objectreturn 0; // 放进来的任务为ScheduledFutureTask类型if (other instanceof ScheduledFutureTask) {ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;// 当前任务的延迟时间与比较任务的延迟时间之差long diff = time - x.time;// 如果小于0,则返回-1if (diff < 0)return -1;// 如果大于0,则返回1else if (diff > 0)return 1;// 如果等于0,则比较任务的序列号,当前任务序列号小则返回-1else if (sequenceNumber < x.sequenceNumber)return -1;// 当前序列号大则返回1elsereturn 1;}// 任务不为ScheduledFutureTask类型,则直接比较两者的延迟时间long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

compareTo(Delayed)方法表明:

  • 当前任务与参数任务(siftUp时是父节点,siftDown时是子节点)比较time属性,即任务的延迟时间;
  • 如果是siftUp,若插入任务延迟时间大于父节点任务延迟时间,则返回1,表明节点不交换,即任务插入当前位置;否则与 父节点任务交换位置并继续比较父节点的父节点;
  • 如果是siftDown,若插入任务延迟时间小于子节点任务延迟时间,则返回-1,表明节点不交换,即任务放置在当前父节点位置;否则与自己诶单任务交换位置并继续比较子节点的子节点;

PS:这个过程需要结合DelayedWorkQueue类的siftUp()和siftDown()方法理解,表明这里使用的是最小堆,即延迟时间最短的任务放置在头部节点,即优先级最高.

1.5 ScheduledFutureTask类的小节

分析ScheduledFutureTask类可知:

  • ScheduledFutureTask类是对Runnable类的一个包装;
  • Runnable任务转变成ScheduledFutureTask任务,使得任务具有周期性,周期任务放入DelayedWorkQueue具有延迟性;
  • ScheduledFutureTask类封装time属性和period属性,分别代表延迟时间和周期时间;
  • ScheduledFutureTask类有三个构造函数,可分别构造周期性任务和非周期性任务;
  • ScheduledFutureTask类重写FutureTask类的run()方法,进而执行周期性任务;
  • ScheduledFutureTask类实现compareTo(T)方法,使得队列中的延迟性任务根据延迟时间的长短排出优先级;

2. ScheduledThreadPoolExecutor

类的主要作用
它是一个可以把提交的任务延迟执行或者周期执行的线程池。比起java.util.Timer类,它更加灵活。延迟任务提交到线程池后开始执行,但具体何时执行则不知道,延迟任务是根据先进先出(FIFO)的提交顺序来执行的。
当提交的任务在运行之前被取消时,执行将被禁止。默认情况下,这样一个被取消的任务不会自动从工作队列中删除,直到它的延迟过期为止。虽然这可以进行进一步的检查和监视,但也可能导致取消的任务无限制地保留。为了避免这种情况,将setRemoveOnCancelPolicy设置为true,这将导致任务在取消时立即从工作队列中删除。
通过scheduleAtFixedRate或scheduleWithFixedDelay调度的任务的连续执行不会重叠。
虽然这个类继承自ThreadPoolExecutor,但是一些继承的调优方法对它并不有用。特别是,由于它使用corePoolSize线程和无界队列(队列最大为Integer.MAX_VALUE)充当固定大小的池,所以对maximumPoolSize的调整没有任何有用的效果。

  • ScheduledThreadPoolExecutor类使用自定义的任务类型,即ScheduledFutureTask;该任务类型可把普通Runnable任务包装成延迟任务,当然也能接受非延迟任务,即设置延迟时间为0.
  • ScheduledThreadPoolExecutor类使用自定义的等待队列,即DelayedWorkQueue;该队列是基于堆实现的优先队列,且队列最大容量为Integer.MAX_VALUE,可被认为是无界队列.与线程池内无界队列搭配的最大线程为Integer.MAX_VALUE.
  • ScheduledThreadPoolExecutor类可以通过属性值控制线程池shutdown后的运行状态;具体属性值看后面的分析.
  • ScheduledThreadPoolExecutor类提供修改任务的装饰方法,默认返回原始任务;具体还没有找到使用场景.
2.1 ScheduledThreadPoolExecutor类的属性
// 线程池停止后,周期任务取消执行则为false,默认为false,不取消则为ture
private volatile boolean continueExistingPeriodicTasksAfterShutdown;// 线程池停止后,取消非周期任务则为false,不取消非周期任务则为true,默认为true
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;// 取消的任务是否移出队列,默认为false不移出
private volatile boolean removeOnCancel = false;// 任务进入队列的序列号
private static final AtomicLong sequencer = new AtomicLong();
2.2 ScheduledThreadPoolExecutor类的构造函数
// 参数corePoolSize为核心线程数
public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}// 参数corePoolSize为核心线程数,threadFactory为自定义线程工厂
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue(), threadFactory);
}//参数corePoolSize为核心线程数,handler为拒绝策略
public ScheduledThreadPoolExecutor(int corePoolSize,RejectedExecutionHandler handler) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue(), handler);
}//参数corePoolSize为核心线程数, threadFactory为自定义线程工厂,handler为拒绝策略
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory,RejectedExecutionHandler handler) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue(), threadFactory, handler);
}

构造函数都是调用父类ThreadPoolExecutor的构造函数,如果不清楚可以看这篇文章的分析。不过,这里可以看出:

  • 最大线程数为Integer.MAX_VALUE;表明线程池内线程数不受限制;
  • 空闲线程的等待时间都为0纳秒,表明池内不存在空闲线程,除了核心线程;
  • 任务等待队列为DelayedWorkQueue,即延迟队列

PS:关于DelayedWorkQueue队列,可以看这篇文章的介绍.

或许此时你想问为什么最大线程数设置为Integer.MAX_VALUE?
如果你知道DelayedWorkQueue队列的原理,你就能理解了。这是因为延迟队列内用数组存放任务,数组初始长度为16,但数组长度会随着任务数的增加而动态扩容,直到数组长度为Integer.MAX_VALUE;既然队列能存放Integer.MAX_VALUE个任务,又因为任务是延迟任务,因此保证任务不被抛弃,最多需要Integer.MAX_VALUE个线程.

那么为什么空闲线程的超时时间设置为0呢?
第三个参数为0,表示空闲线程超时时间 ,第四个参数为前一个参数的时间位;ScheduledThreadPoolExecutor的四个构造函数,空闲线程超时时间都为0,表示池内不存在空闲线程.
如何定义池内的空闲线程是关键.我们知道ScheduledThreadPoolExecutor线程池会把池内的某一个线程定义为leader线程,该leader线程用于等待队列的根节点直到获取并运行任务,而其他线程则会阻塞等待;阻塞等待的线程等待leader线程释放唤醒的信号,等待队列中的某个线程会被升级为leader线程,其他线程继续等待.那么,这里等待的线程都为空闲线程,为了避免过多的线程浪费资源,所以ScheduledThreadPool线程池内更多的存活的是核心线程.

PS:核心线程数的设置应该跟任务的周期时间和实际执行完成的时间有关.若任务周期短,但执行时间长,则核心线程数设置大一点,这样避免线程频繁的创建和销毁.若任务周期长,但执行时间短,则核心线程数设置小一点,避免核心线程数内存活过多的空闲线程.

2.4 ScheduledThreadPoolExecutor类的schedule(Runnable,long,TimeUnit)

ScheduledThreadPoolExecutor类的execute(Runnable)和submit(Runnable)方法都是调用schedule(Runnable,long,TimeUnit)方法.
首先看下被重写的execute(Runnable)方法:

public void execute(Runnable command) {schedule(command, 0, NANOSECONDS);
}
public Future<?> submit(Runnable task) {return schedule(task, 0, NANOSECONDS);
}
public <T> Future<T> submit(Runnable task, T result) {return schedule(Executors.callable(task, result), 0, NANOSECONDS);
}
public <T> Future<T> submit(Callable<T> task) {return schedule(task, 0, NANOSECONDS);
}

通过正常方法提交的任务, 内部调用的都是schedule(Runnable,long,TimeUnit);方法第一个参数就是执行的任务,第二个参数都为0,表示任务的延迟时间为0,第三个参数为前一个参数的时间单位.

public ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit) {if (command == null || unit == null)throw new NullPointerException();RunnableScheduledFuture<?> t = decorateTask(command,new ScheduledFutureTask<Void>(command, null,triggerTime(delay, unit)));delayedExecute(t);return t;
}

参数含义:

  • command:执行任务;
  • delay:延迟时间;
  • unit:延迟时间的单位,有七种;一般使用SECONDS和MILLISECONDS;

执行过程:

  • 若Runnable任务为null,则抛出NPE异常;
  • 根据delay和unit参数计算延迟任务的触发时间;我们这里delay值都为0,表示非延迟任务;
  • 把Runnable类型的任务转变成ScheduledFutureTask类型;第二个参数表示任务返回类型,这里设置为null;
  • decorateTask(Runnable,ScheduledFutureTask)默认返回ScheduledFutureTask任务.该方法作用是修改或替换用于执行的任务,可覆盖用于管理内部任务的具体类;默认实现只返回给定的任务。
  • delayedExecute(RunnableScheduledFuture)执行提交的ScheduledFutureTask任务;

分析schedule执行过程可知,首先Runnable任务需要转变成ScheduledFutureTask任务,然后该任务由delayedExecute(RunnableScheduledFuture)方法执行.

以上方法提交到线程池的任务都是普通任务,属于非延迟任务.

delayedExecute(RunnableScheduledFuture)方法如何执行任务?

private void delayedExecute(RunnableScheduledFuture<?> task) {if (isShutdown())// 线程池状态为shutdown,则执行拒绝策略拒绝任务reject(task);else {// 线程状态正常,则把任务放入优先队列super.getQueue().add(task);// shutdown状态下,非周期任务不会移除队列if (isShutdown() &&!canRunInCurrentRunState(task.isPeriodic()) &&remove(task))// shutdown状态下,周期任务会默认移除队列task.cancel(false);else// 如果池内线程数小于核心线程数,则新建一个线程ensurePrestart();}
}

队列中的任务等待leader线程消费任务.

目前为止,我们讨论的都是普通任务,而么有讨论周期性任务.ScheduledThreadPool线程池通过下面两个方法提交周期任务并处理周期任务.也是该线程池的核心任务.

2.5 ScheduledThreadPoolExecutor类的scheduledAtFixedRate(Runnable,long,long,TimeUnit)

该方法创建一个周期性任务,且设置任务的首次执行时间,即延迟时间;方法参数:

  • command:需要执行的周期任务;
  • initialDelay:初始化延迟时间;根据该参数计算出任务的首次执行时间;
  • period:周期时间;任务周期化执行的时间;根据该参数计算出任务的后续执行时间;
  • unit:第二和第三个参数的时间单位;
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;
}

方法执行逻辑:

  • 若Runnable任务和unit值为null,则抛出NPE异常;
  • 确保period参数大于0,否则抛出非法参数异常;首先period参数不能为0,不然和方法schedule(Runnable,long,TimeUnit)没区别;然后period参数不能小于0,period参数本身的含义是周期性,若小于0 则没有任何意义;
  • Runnable任务封装成ScheduledFutureTask任务;确定任务的延迟时间和周期时间;
  • decorateTask()方法默认返回sft参数;方法可以修改或者取代任务;
  • outerTask属性值是ScheduledFutureTask任务run()方法的处理的任务;
  • delayedExecute(t)会把任务加入延迟队列并根据线程池内线程数判断是否新建线程;该方法的具体理解见这片文章;

这里可能会问,如果任务的执行时间超过周期时间会如何?
关于这个问题的答案应该是,任务的执行时间和周期性没有任何关系;即会按照固定的周期时间设置该任务的下次执行时间,然后放入优先队列;优先队列最大深度为Integer.MAX_VALUE,线程池最大线程数也为Integer.MAX_VALUE.
注意:由于线程在执行该任务,即拥有该Runnable对象的锁,因此即使下一次runnable对象的延迟时间到期也会因为锁没有被释放而无法执行;所以,表现出来的现象是只有该runnable任务执行完,才会开始执行下一次的任务。

2.6 ScheduledThreadPoolExecutor类的ScheduleWithFixedDelay (Runnable,long,long,TimeUnit)

ScheduleWithFixedDelay()方法创建并执行一个周期性的Runnable方法,该方法会在给定的初始化延迟时间之后执行;Runnable方法执行结束之后,在给定的delay值(延迟时间)后再次执行;该周期性操作会在Runnable方法执行出现异常时停止,或者线程池终止时停止.

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();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;
}

ScheduleWithFixedDelay与ScheduleAtFixedRate的区别在于Runnable方法的执行方式;前者是方法执行结束之后,延迟一段时间之后再执行下一次,表明前后两次执行关系紧密;后者时不管前一次方法执行有没有结束,在固定的时间后都会再次重复该方法;当然,由于两者的Runnable任务都是延迟任务,因此任务都会加入到优先队列中等候.
既然是这样,那么如果使用ScheduleWithFixedDelay()方法,核心线程数尽量取小一点,这样可以避免空闲线程.

3. 总结

围绕ScheduledThreadPoolExecutor类来讲,讲到内部任务类ScheduledFutureTask的属性和方法;然后讲到周期线程池的属性和方法,特别强调ScheduleWithFixedDelay()和ScheduleAtFixedRate()方法的使用和区别.

面试|详细分析ScheduledThreadPoolExecutor(周期性线程池)的原理相关推荐

  1. ScheduledThreadPoolExecutor定时任务线程池执行原理分析

    一.示例代码 @Slf4j public class ScheduleThreadPoolTest {private static ScheduledExecutorService executor ...

  2. SpringBoot——@Scheduled的自定义周期性线程池解决任务延时执行问题

    关注微信公众号:CodingTechWork,一起学习进步. 问题   在使用Spring中的@Scheduled注解设置定时任务时,遇到这样2个问题: 定时任务未按时执行,现象是延后了一段时间才执行 ...

  3. Java 线程池(ThreadPoolExecutor)原理分析与使用

    ThreadPoolExecutor原理概述 在我们的开发中"池"的概念并不罕见,有数据库连接池.线程池.对象池.常量池等等.下面我们主要针对线程池来一步一步揭开线程池的面纱. 使 ...

  4. Java线程池ThreadPoolExecutor使用和分析(三) - 终止线程池原理

    相关文章目录: Java线程池ThreadPoolExecutor使用和分析(一) Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理 Java线程池Thr ...

  5. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

  6. Java 线程池(ThreadPoolExecutor)原理分析与使用 – 码农网

    线程池的详解 Java 线程池(ThreadPoolExecutor)原理分析与使用 – 码农网 http://www.codeceo.com/article/java-threadpool-exec ...

  7. 【有料】Java线程池实现原理及其在美团业务中的实践

    随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流.使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器.J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员 ...

  8. 写的很好!细数 Java 线程池的原理

    今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPoolExecutor类中的方法讲起,然后再讲述它的实现原理,接着给出了它的使用示例,最后讨论了一下如何合理配置线程池的大小. ...

  9. 一文带你清晰弄明白线程池的原理

    不知道你是否还记得阿里巴巴的java代码规范中对多线程有这样一条强制规范: [强制]线程资源必须通过线程池提供,不允许在程序中显示创建线程. 说明:使用线程池的好处是减少在创建和销毁线程池上所消耗的时 ...

最新文章

  1. 存储过程-数据位置调换demo
  2. python如何下载tushare_安装tushare
  3. 中后台管理信息系统通用原型方案_AxureUX客户关系管理系统后台设置中心原型模板正式发布...
  4. android 判断时间是否过期_眉笔眉粉有保质期吗?怎么判断眉笔眉粉是否过期?...
  5. 《Outlook时间整理术》一创建和使用自己的文件夹结构
  6. 工具推荐-css3渐变生成工具
  7. 中级软件测试笔试题100精讲_汇集上千位软件测试精英面试笔试题,最全面的题型都在这!...
  8. J2EE(环境搭建)
  9. Vue 实例常用的属性和方法
  10. win10c盘android,Win10系统C盘哪些文件可以删除?C盘无用文件都在哪?
  11. 爱奇艺开源FASPell项目
  12. 腾讯2017年度代码报告:程序员15500人、年撸码5亿行、手Q代码已过百万行
  13. R语言--rep函数
  14. C++实验4-项目7穷举法解决组合问题-百钱百鸡问题
  15. Vissim全网最全学习资料入口
  16. 如何通过“云之讯”平台,完成短信的发送
  17. 支持向量机(SVM)学习小记
  18. slide3D插件版本更新
  19. FPGA:逻辑代数的基本公式和规则
  20. php 弹出php startup警告解决办法

热门文章

  1. 非常不错的停用词词表
  2. ROS学习记录16【SLAM】仿真学习5——将cmd_vel转换为ackman小车的速度
  3. 简述igp和egp_igp egp
  4. java 泛型中的上界(extend)和下界(super)
  5. java学生成绩降序代码_输入5名学员成绩,降序排列输出
  6. 2-14-Multiple Exemplars-based Hallucination for Face Super-resolution and Editing(ACCV2020)
  7. vray渲染出图尺寸_3DMax渲染出图尺寸怎么设置?
  8. hp 800 g4 twr linux,【拆机】HP EliteDesk 800 G4 TWR—探究塔式机箱的秘密
  9. 【转】阿里云主机购买使用教程
  10. java中的steam流