在上一篇线程池的文章《并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)》中从ThreadPoolExecutor源码分析了其运行机制。限于篇幅,留下了ScheduledThreadPoolExecutor未做分析,因此本文继续从源代码出发分析ScheduledThreadPoolExecutor的内部原理。

类声明

  1. public class ScheduledThreadPoolExecutor
  2. extends ThreadPoolExecutor
  3. implements ScheduledExecutorService {

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,实现了ScheduledExecutorService。因此它具有ThreadPoolExecutor的所有能力。所不同的是它具有定时执行,以周期或间隔循环执行任务等功能。

这里我们先看下ScheduledExecutorService的源码:

ScheduledExecutorService

  1. //可调度的执行者服务接口
  2. public interface ScheduledExecutorService extends ExecutorService {
  3.  
  4. //指定时延后调度执行任务,只执行一次,没有返回值
  5. public ScheduledFuture<?> schedule(Runnable command,
  6. long delay, TimeUnit unit);
  7.  
  8. //指定时延后调度执行任务,只执行一次,有返回值
  9. public <V> ScheduledFuture<V> schedule(Callable<V> callable,
  10. long delay, TimeUnit unit);
  11.  
  12. //指定时延后开始执行任务,以后每隔period的时长再次执行该任务
  13. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
  14. long initialDelay,
  15. long period,
  16. TimeUnit unit);
  17.  
  18. //指定时延后开始执行任务,以后任务执行完成后等待delay时长,再次执行任务
  19. public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
  20. long initialDelay,
  21. long delay,
  22. TimeUnit unit);
  23. }

其中schedule方法用于单次调度执行任务。这里主要理解下后面两个方法。

  • scheduleAtFixedRate:该方法在initialDelay时长后第一次执行任务,以后每隔period时长,再次执行任务。注意,period是从任务开始执行算起的。开始执行任务后,定时器每隔period时长检查该任务是否完成,如果完成则再次启动任务,否则等该任务结束后才再次启动任务,看下图示例

  • scheduleWithFixDelay:该方法在initialDelay时长后第一次执行任务,以后每当任务执行完成后,等待delay时长,再次执行任务,看下图示例。

使用例子

1、schedule(Runnable command,long delay, TimeUnit unit)

  1. /**
  2. * @author: ChenHao
  3. * @Date: Created in 14:54 2019/1/11
  4. */
  5. public class Test1 {
  6. public static void main(String[] args) throws ExecutionException, InterruptedException {
  7. // 延迟1s后开始执行,只执行一次,没有返回值
  8. ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10);
  9. ScheduledFuture<?> result = executorService.schedule(new Runnable() {
  10. @Override
  11. public void run() {
  12. System.out.println("gh");
  13. try {
  14. Thread.sleep(3000);
  15. } catch (InterruptedException e) {
  16. // TODO Auto-generated catch block
  17. e.printStackTrace();
  18. }
  19. }
  20. }, 1000, TimeUnit.MILLISECONDS);
  21. System.out.println(result.get());
  22. }
  23. }

运行结果:

2、schedule(Callable<V> callable, long delay, TimeUnit unit);

  1. public class Test2 {
  2. public static void main(String[] args) throws ExecutionException, InterruptedException {
  3. // 延迟1s后开始执行,只执行一次,有返回值
  4. ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10);
  5. ScheduledFuture<String> result = executorService.schedule(new Callable<String>() {
  6. @Override
  7. public String call() throws Exception {
  8. try {
  9. Thread.sleep(3000);
  10. } catch (InterruptedException e) {
  11. // TODO Auto-generated catch block
  12. e.printStackTrace();
  13. }
  14. return "ghq";
  15. }
  16. }, 1000, TimeUnit.MILLISECONDS);
  17. // 阻塞,直到任务执行完成
  18. System.out.print(result.get());
  19. }
  20. }

运行结果:

3、scheduleAtFixedRate

  1. /**
  2. * @author: ChenHao
  3. * @Date: Created in 14:54 2019/1/11
  4. */
  5. public class Test3 {
  6. public static void main(String[] args) throws ExecutionException, InterruptedException {
  7. ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10);
  8. // 从加入任务开始算1s后开始执行任务,1+2s开始执行,1+2*2s执行,1+n*2s开始执行;
  9. // 但是如果执行任务时间大于2s则不会并发执行后续任务,当前执行完后,接着执行下次任务。
  10. ScheduledFuture<?> result = executorService.scheduleAtFixedRate(new Runnable() {
  11. @Override
  12. public void run() {
  13. System.out.println(System.currentTimeMillis());
  14. }
  15. }, 1000, 2000, TimeUnit.MILLISECONDS);
  16.  
  17. //一个ScheduledExecutorService里可以同时添加多个定时任务,这样就是形成堆
  18. ScheduledFuture<?> result2 = executorService.scheduleAtFixedRate(new Runnable() {
  19. @Override
  20. public void run() {
  21. System.out.println(System.currentTimeMillis());
  22. }
  23. }, 1000, 2000, TimeUnit.MILLISECONDS);
  24. }
  25. }

这里可以看到一个ScheduledExecutorService 中可以添加多个定时任务,这是就会形成堆

运行结果:

4、scheduleWithFixedDelay

  1. /**
  2. * @author: ChenHao
  3. * @Date: Created in 14:54 2019/1/11
  4. */
  5. public class Test4 {
  6. public static void main(String[] args) throws ExecutionException, InterruptedException {
  7. //任务间以固定时间间隔执行,延迟1s后开始执行任务,任务执行完毕后间隔2s再次执行,任务执行完毕后间隔2s再次执行,依次往复
  8. ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(10);
  9. ScheduledFuture<?> result = executorService.scheduleWithFixedDelay(new Runnable() {
  10. @Override
  11. public void run() {
  12. System.out.println(System.currentTimeMillis());
  13. }
  14. }, 1000, 2000, TimeUnit.MILLISECONDS);
  15.  
  16. // 由于是定时任务,一直不会返回
  17. result.get();
  18. System.out.println("over");
  19. }
  20. }

运行结果:

源码分析

构造器

  1. public ScheduledThreadPoolExecutor(int corePoolSize) {
  2. super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
  3. new DelayedWorkQueue());
  4. }

内部其实都是调用了父类ThreadPoolExecutor的构造器,因此它具有ThreadPoolExecutor的所有能力。

通过super方法的参数可知,核心线程的数量即传入的参数,而线程池的线程数为Integer.MAX_VALUE,几乎为无上限。
 这里采用了DelayedWorkQueue任务队列,也是定时任务的核心,是一种优先队列,时间小的排在前面,所以获取任务的时候就能先获取到时间最小的执行,可以看我上篇文章《并发编程(十四)—— ScheduledThreadPoolExecutor 实现原理与源码深度解析 之 DelayedWorkQueue》。

由于这里队列没有定义大小,所以队列不会添加满,因此最大的线程数就是核心线程数,超过核心线程数的任务就放在队列里,并不重新开启临时线程。

我们先来看看几个入口方法的实现:

  1. public ScheduledFuture<?> schedule(Runnable command,
  2. long delay,
  3. TimeUnit unit) {
  4. if (command == null || unit == null)
  5. throw new NullPointerException();
  6. RunnableScheduledFuture<?> t = decorateTask(command,
  7. new ScheduledFutureTask<Void>(command, null,
  8. triggerTime(delay, unit)));
  9. delayedExecute(t);
  10. return t;
  11. }
  12.  
  13. public <V> ScheduledFuture<V> schedule(Callable<V> callable,
  14. long delay,
  15. TimeUnit unit) {
  16. if (callable == null || unit == null)
  17. throw new NullPointerException();
  18. RunnableScheduledFuture<V> t = decorateTask(callable,
  19. new ScheduledFutureTask<V>(callable,
  20. triggerTime(delay, unit)));
  21. delayedExecute(t);
  22. return t;
  23. }
  24.  
  25. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
  26. long initialDelay,
  27. long period,
  28. TimeUnit unit) {
  29. if (command == null || unit == null)
  30. throw new NullPointerException();
  31. if (period <= 0)
  32. throw new IllegalArgumentException();
  33. ScheduledFutureTask<Void> sft =
  34. new ScheduledFutureTask<Void>(command,
  35. null,
  36. triggerTime(initialDelay, unit),
  37. unit.toNanos(period));
  38. RunnableScheduledFuture<Void> t = decorateTask(command, sft);
  39. sft.outerTask = t;
  40. delayedExecute(t);
  41. return t;
  42. }
  43.  
  44. public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
  45. long initialDelay,
  46. long delay,
  47. TimeUnit unit) {
  48. if (command == null || unit == null)
  49. throw new NullPointerException();
  50. if (delay <= 0)
  51. throw new IllegalArgumentException();
  52. ScheduledFutureTask<Void> sft =
  53. new ScheduledFutureTask<Void>(command,
  54. null,
  55. triggerTime(initialDelay, unit),
  56. unit.toNanos(-delay));
  57. RunnableScheduledFuture<Void> t = decorateTask(command, sft);
  58. sft.outerTask = t;
  59. delayedExecute(t);
  60. return t;
  61. }

这几个方法都是将任务封装成了ScheduledFutureTask,上面做的首先把runnable装饰为delay队列所需要的格式的元素,然后把元素加入到阻塞队列,然后线程池线程会从阻塞队列获取超时的元素任务进行处理,下面看下队列元素如何实现的。

ScheduledFutureTask

ScheduledFutureTask是一个延时定时任务,它可以返回任务剩余延时时间,可以被周期性地执行。

属性

  1. private class ScheduledFutureTask<V>
  2. extends FutureTask<V> implements RunnableScheduledFuture<V> {
  3. /** 是一个序列,每次创建任务的时候,都会自增。 */
  4. private final long sequenceNumber;
  5.  
  6. /** 任务能够开始执行的时间 */
  7. private long time;
  8.  
  9. /**
  10. * 任务周期执行的时间
  11. * 0表示不是一个周期定时任务
  12. * 正数表示固定周期时间去执行任务
  13. * 负数表示任务完成之后,延时period时间再去执行任务
  14. */
  15. private final long period;
  16.  
  17. /** 表示再次执行的任务,在reExecutePeriodic中调用 */
  18. RunnableScheduledFuture<V> outerTask = this;
  19.  
  20. /**
  21. * 表示在任务队列中的索引位置,用来支持快速从队列中删除任务。
  22. */
  23. int heapIndex;
  24. }

ScheduledFutureTask继承了 FutureTask 和 RunnableScheduledFuture

属性说明:

  1. sequenceNumber: 是一个序列,每次创建任务的时候,都会自增。
  2. time: 任务能够开始执行的时间。
  3. period: 任务周期执行的时间。0表示不是一个周期定时任务。
  4. outerTask: 表示再次执行的任务,在reExecutePeriodic中调用
  5. heapIndex: 表示在任务队列中的索引位置,用来支持快速从队列中删除任务。

构造器

  • 创建延时任务

  1. /**
  2. * 创建延时任务
  3. */
  4. ScheduledFutureTask(Runnable r, V result, long ns) {
  5. // 调用父类的方法
  6. super(r, result);
  7. // 任务开始的时间
  8. this.time = ns;
  9. // period是0,不是一个周期定时任务
  10. this.period = 0;
  11. // 每次创建任务的时候,sequenceNumber都会自增
  12. this.sequenceNumber = sequencer.getAndIncrement();
  13. }
  14.  
  15. /**
  16. * 创建延时任务
  17. */
  18. ScheduledFutureTask(Callable<V> callable, long ns) {
  19. // 调用父类的方法
  20. super(callable);
  21. // 任务开始的时间
  22. this.time = ns;
  23. // period是0,不是一个周期定时任务
  24. this.period = 0;
  25. // 每次创建任务的时候,sequenceNumber都会自增
  26. this.sequenceNumber = sequencer.getAndIncrement();
  27. }

我们看看super(),其实就是FutureTask 里面的构造方法,关于FutureTask 可以看看我之前的文章《Java 多线程(五)—— 线程池基础 之 FutureTask源码解析

  1. public FutureTask(Runnable runnable, V result) {
  2. this.callable = Executors.callable(runnable, result);
  3. this.state = NEW; // ensure visibility of callable
  4. }
  5. public FutureTask(Callable<V> callable) {
  6. if (callable == null)
  7. throw new NullPointerException();
  8. this.callable = callable;
  9. this.state = NEW; // ensure visibility of callable
  10. }
  • 创建延时定时任务
  1. /**
  2. * 创建延时定时任务
  3. */
  4. ScheduledFutureTask(Runnable r, V result, long ns, long period) {
  5. // 调用父类的方法
  6. super(r, result);
  7. // 任务开始的时间
  8. this.time = ns;
  9. // 周期定时时间
  10. this.period = period;
  11. // 每次创建任务的时候,sequenceNumber都会自增
  12. this.sequenceNumber = sequencer.getAndIncrement();
  13. }

延时定时任务不同的是设置了period,后面通过判断period是否为0来确定是否是定时任务。

run()

  1. public void run() {
  2. // 是否是周期任务
  3. boolean periodic = isPeriodic();
  4. // 如果不能在当前状态下运行,那么就要取消任务
  5. if (!canRunInCurrentRunState(periodic))
  6. cancel(false);
  7. // 如果只是延时任务,那么就调用run方法,运行任务。
  8. else if (!periodic)
  9. ScheduledFutureTask.super.run();
  10. // 如果是周期定时任务,调用runAndReset方法,运行任务。
  11. // 这个方法不会改变任务的状态,所以可以反复执行。
  12. else if (ScheduledFutureTask.super.runAndReset()) {
  13. // 设置周期任务下一次执行的开始时间time
  14. setNextRunTime();
  15. // 重新执行任务outerTask
  16. reExecutePeriodic(outerTask);
  17. }
  18. }

这个方法会在ThreadPoolExecutor的runWorker方法中调用,而且这个方法调用,说明肯定已经到了任务的开始时间time了。这个方法我们待会会再继续来回看一下

  1. 先判断当前线程状态能不能运行任务,如果不能,就调用cancel()方法取消本任务。
  2. 如果任务只是一个延时任务,那么调用父类的run()运行任务,改变任务的状态,表示任务已经运行完成了。
  3. 如果任务只是一个周期定时任务,那么就任务必须能够反复执行,那么就不能调用run()方法,它会改变任务的状态。而是调用runAndReset()方法,只是简单地运行任务,而不会改变任务状态。
  4. 设置周期任务下一次执行的开始时间time,并重新执行任务。

schedule(Runnable command, long delay,TimeUnit unit)

  1. public ScheduledFuture<?> schedule(Runnable command,
  2. long delay,
  3. TimeUnit unit) {
  4. if (command == null || unit == null)
  5. throw new NullPointerException();
  6.  
  7. //装饰任务,主要实现public long getDelay(TimeUnit unit)和int compareTo(Delayed other)方法
  8. RunnableScheduledFuture<?> t = decorateTask(command,
  9. new ScheduledFutureTask<Void>(command, null,
  10. triggerTime(delay, unit)));
  11. //添加任务到延迟队列
  12. delayedExecute(t);
  13. return t;
  14. }

获取延时执行时间

  1. private long triggerTime(long delay, TimeUnit unit) {
  2. return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
  3. }
  4.  
  5. /**
  6. * Returns the trigger time of a delayed action.
  7. */
  8. long triggerTime(long delay) {
  9. //当前时间加上延时时间
  10. return now() +
  11. ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
  12. }

上述的decorateTask方法把Runnable任务包装成ScheduledFutureTask,用户可以根据自己的需要覆写该方法:

  1. protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable, RunnableScheduledFuture<V> task) {
  2. return task;
  3. }

schedule的核心是其中的delayedExecute方法:

  1. private void delayedExecute(RunnableScheduledFuture<?> task) {
  2. if (isShutdown()) // 线程池已关闭
  3. reject(task); // 任务拒绝策略
  4. else {
  5. //将任务添加到任务队列,会根据任务延时时间进行排序
  6. super.getQueue().add(task);
  7. // 如果线程池状态改变了,当前状态不能运行任务,那么就尝试移除任务,
  8. // 移除成功,就取消任务。
  9. if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task))
  10. task.cancel(false); // 取消任务
  11. else
  12. // 预先启动工作线程,确保线程池中有工作线程。
  13. ensurePrestart();
  14. }
  15. }

这个方法的主要作用就是将任务添加到任务队列中,因为这里任务队列是优先级队列DelayedWorkQueue,它会根据任务的延时时间进行排序。

  • 如果线程池不是RUNNING状态,不能执行延时任务task,那么调用reject(task)方法,拒绝执行任务task。

  • 将任务添加到任务队列中,会根据任务的延时时间进行排序。

  • 因为是多线程并发环境,就必须判断在添加任务的过程中,线程池状态是否被别的线程更改了,那么就可能要取消任务了。

  • 将任务添加到任务队列后,还要确保线程池中有工作线程,不然任务也不为执行。所以ensurePrestart()方法预先启动工作线程,确保线程池中有工作线程。

  1. void ensurePrestart() {
  2. // 线程池中的线程数量
  3. int wc = workerCountOf(ctl.get());
  4. // 如果小于核心池数量,就创建新的工作线程
  5. if (wc < corePoolSize)
  6. addWorker(null, true);
  7. // 说明corePoolSize数量是0,必须创建一个工作线程来执行任务
  8. else if (wc == 0)
  9. addWorker(null, false);
  10. }

通过ensurePrestart可以看到,如果核心线程池未满,则新建的工作线程会被放到核心线程池中。如果核心线程池已经满了,ScheduledThreadPoolExecutor不会像ThreadPoolExecutor那样再去创建归属于非核心线程池的工作线程,加入到队列就完了,等待核心线程执行完任务再拉取队列里的任务。也就是说,在ScheduledThreadPoolExecutor中,一旦核心线程池满了,就不会再去创建工作线程。

这里思考一点,什么时候会执行else if (wc == 0)创建一个归属于非核心线程池的工作线程?
答案是,当通过setCorePoolSize方法设置核心线程池大小为0时,这里必须要保证任务能够被执行,所以会创建一个工作线程,放到非核心线程池中。

看到 addWorker(null, true); 并没有将任务设置进入,而是设置的null, 则说明线程池里线程第一次启动时, runWorker中取到的 firstTask为null,需要通过 getTask() 从队列中取任务,这里可以看看我之前写的关于线程池的文章《并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)》。

getTask()中  Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();如果是存在核心线程则调用take(),如果传入的核心线程为0,则存在一个临时线程,调用poll(),这两个方法都会先获取时间,看看有没有达到执行时间,没有达到执行时间则阻塞,可以看看我上一篇文章,达到执行时间,则取到任务,就会执行下面的run方法。

  1. public void run() {
  2. // 是否是周期任务
  3. boolean periodic = isPeriodic();
  4. // 如果不能在当前状态下运行,那么就要取消任务
  5. if (!canRunInCurrentRunState(periodic))
  6. cancel(false);
  7. // 如果只是延时任务,那么就调用run方法,运行任务。
  8. else if (!periodic)
  9. ScheduledFutureTask.super.run();
  10. // 如果是周期定时任务,调用runAndReset方法,运行任务。
  11. // 这个方法不会改变任务的状态,所以可以反复执行。
  12. else if (ScheduledFutureTask.super.runAndReset()) {
  13. // 设置周期任务下一次执行的开始时间time
  14. setNextRunTime();
  15. // 重新执行任务outerTask
  16. reExecutePeriodic(outerTask);
  17. }
  18. }
  19.  
  20. public boolean isPeriodic() {
  21. return period != 0;
  22. }

schedule不是周期任务,那么调用父类的run()运行任务,改变任务的状态,表示任务已经运行完成了。

scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)

  1. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
  2. long initialDelay,
  3. long period,
  4. TimeUnit unit) {
  5. if (command == null || unit == null)
  6. throw new NullPointerException();
  7. if (period <= 0)
  8. throw new IllegalArgumentException();
  9. //装饰任务类,注意period=period>0,不是负的
  10. ScheduledFutureTask<Void> sft =
  11. new ScheduledFutureTask<Void>(command,
  12. null,
  13. triggerTime(initialDelay, unit),
  14. unit.toNanos(period));
  15. RunnableScheduledFuture<Void> t = decorateTask(command, sft);
  16. sft.outerTask = t;
  17. //添加任务到队列
  18. delayedExecute(t);
  19. return t;
  20. }

如果是周期任务则执行上面run()方法中的第12行,调用父类中的runAndReset(),这个方法同run方法比较的区别是call方法执行后不设置结果,因为周期型任务会多次执行,所以为了让FutureTask支持这个特性除了发生异常不设置结果。

执行完任务后通过setNextRunTime方法计算下一次启动时间:

  1. private void setNextRunTime() {
  2. long p = period;
  3. //period=delay;
  4. if (p > )
  5. time += p;//由于period>0所以执行这里,设置time=time+delay
  6. else
  7. time = triggerTime(-p);
  8. }
  9.  
  10. long triggerTime(long delay) {
  11. return now() +
  12. ((delay < (Long.MAX_VALUE >> )) ? delay : overflowFree(delay));
  13. }

scheduleAtFixedRate会执行到情况一,下一次任务的启动时间最早为上一次任务的启动时间加period。
scheduleWithFixedDelay会执行到情况二,这里很巧妙的将period参数设置为负数到达这段代码块,在此又将负的period转为正数。情况二将下一次任务的启动时间设置为当前时间加period。

然后将任务再次添加到任务队列:

  1. /**
  2. * 重新执行任务task
  3. */
  4. void reExecutePeriodic(RunnableScheduledFuture<?> task) {
  5. // 判断当前线程池状态能不能运行任务
  6. if (canRunInCurrentRunState(true)) {
  7. // 将任务添加到任务队列,会根据任务延时时间进行排序
  8. super.getQueue().add(task);
  9. // 如果线程池状态改变了,当前状态不能运行任务,那么就尝试移除任务,
  10. // 移除成功,就取消任务。
  11. if (!canRunInCurrentRunState(true) && remove(task))
  12. task.cancel(false);
  13. else
  14. // 预先启动工作线程,确保线程池中有工作线程。
  15. ensurePrestart();
  16. }
  17. }

这个方法与delayedExecute方法很像,都是将任务添加到任务队列中。

  1. 如果当前线程池状态能够运行任务,那么任务添加到任务队列。
  2. 如果在在添加任务的过程中,线程池状态是否被别的线程更改了,那么就要进行判断,是否需要取消任务。
  3. 调用ensurePrestart()方法,预先启动工作线程,确保线程池中有工作线程。

ScheduledFuture的get方法

既然ScheduledFuture的实现是ScheduledFutureTask,而ScheduledFutureTask继承自FutureTask,所以ScheduledFuture的get方法的实现就是FutureTask的get方法的实现,FutureTask的get方法的实现分析在ThreadPoolExecutor篇已经写过,这里不再叙述。要注意的是ScheduledFuture的get方法对于非周期任务才是有效的。

ScheduledThreadPoolExecutor总结

  • ScheduledThreadPoolExecutor和ThreadPoolExecutor的区别:

    ThreadPoolExecutor每次addwoker就会将自己的Task传进新创建的woker中的线程执行,因此woker会第一时间执行当前Task,只有线程数超过了核心线程才会将任务放进队列里

    ScheduledThreadPoolExecutor是直接入队列,并且创建woker时传到woker的是null,说明woker中的线程刚启动时并没有任务执行,只能通过getTask去队列里取任务,取任务时会判断是否到了执行时间,因此具有了延时执行的特性,并且task执行完了,会将当前任务重新放进堆里,并设置下次执行的时间。

  • ScheduledThreadPoolExecutor是实现自ThreadPoolExecutor的线程池,构造方法中传入参数n,则最多会有n个核心线程工作,空闲的核心线程不会被自动终止,而是一直阻塞在DelayedWorkQueue的take方法尝试获取任务。构造方法传入的参数为0,ScheduledThreadPoolExecutor将以非核心线程工作,并且最多只会创建一个非核心线程,参考上文中ensurePrestart方法的执行过程。而这个非核心线程以poll方法获取定时任务之所以不会因为超时就被回收,是因为任务队列并不为空,只有在任务队列为空时才会将空闲线程回收,详见ThreadPoolExecutor篇的runWorker方法,之前我以为空闲的非核心线程超时就会被回收是不正确的,还要具备任务队列为空这个条件。

  • ScheduledThreadPoolExecutor的定时执行任务依赖于DelayedWorkQueue,其内部用可扩容的数组实现以启动时间升序的二叉树。

  • 工作线程尝试获取DelayedWorkQueue的任务只有在任务到达指定时间才会成功,否则非核心线程会超时返回null,核心线程一直阻塞。

  • 对于非周期型任务只会执行一次并且可以通过ScheduledFuture的get方法阻塞得到结果,其内部实现依赖于FutureTask的get方法。

  • 周期型任务通过get方法无法获取有效结果,因为FutureTask对于周期型任务执行的是runAndReset方法,并不会设置结果。周期型任务执行完毕后会重新计算下一次启动时间并且再次添加到DelayedWorkQueue中,所有的Task会公用一个队列,如果一个定时器里添加多个任务,此时就会形成堆,如果只是一个定时任务,则每次只有堆顶一个数据,并且也只需要一个核心线程就够用了,因为只有当前任务执行完才会再将该任务添加到堆里。

并发编程(十五)——定时器 ScheduledThreadPoolExecutor 实现原理与源码深度解析的更多相关文章

  1. 并发编程(十四)—— ScheduledThreadPoolExecutor 实现原理与源码深度解析 之 DelayedWorkQueue

    我们知道线程池运行时,会不断从任务队列中获取任务,然后执行任务.如果我们想实现延时或者定时执行任务,重要一点就是任务队列会根据任务延时时间的不同进行排序,延时时间越短地就排在队列的前面,先被获取执行. ...

  2. 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)

    在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...

  3. 并发编程(十三)—— Java 线程池 实现原理与源码深度解析 之 Executors(三)

    前两篇文章讲了线程池的源码分析,再来看这篇文章就比较简单了, 本文主要讲解 Executors 这个工具类,看看长江创建线程池的几种方法. newFixedThreadPool 生成一个固定大小的线程 ...

  4. JVM CPU Profiler技术原理及源码深度解析

    研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程序运行行为和性能瓶颈.Profiling技术是一种在应用运行时收集程序相关信息的动态分析手段,常用的JVM Profiler可以从多个方面对程 ...

  5. 并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)

    史上最清晰的线程池源码分析 鼎鼎大名的线程池.不需要多说!!!!! 这篇博客深入分析 Java 中线程池的实现. 总览 下图是 java 线程池几个相关类的继承结构:    先简单说说这个继承结构,E ...

  6. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

  7. 并发编程学习笔记(8)----ThreadLocal的使用及源码分析

    1. ThreadLocal的理解 ThreadLocal,顾名思义,就是线程的本地变量,ThreadLocal会为每个线程创建一个本地变量副本,使得使用ThreadLocal管理的变量在多线程的环境 ...

  8. spring5 源码深度解析----- 被面试官给虐懵了,竟然是因为我不懂@Configuration配置类及@Bean的原理

    @Configuration注解提供了全新的bean创建方式.最初spring通过xml配置文件初始化bean并完成依赖注入工作.从spring3.0开始,在spring framework模块中提供 ...

  9. Thrift之代码生成器Compiler原理及源码详细解析1

    我的新浪微博:http://weibo.com/freshairbrucewoo. 欢迎大家相互交流,共同提高技术. 又很久没有写博客了,最近忙着研究GlusterFS,本来周末打算写几篇博客的,但是 ...

随机推荐

  1. Python2出现SyntaxError: Non-ASCII character '\xe5' in file *******

    在使用Python2编写Python时,当使用中文输出或注释时运行脚本,会提示错误信息:SyntaxError: Non-ASCII character '\xe5' in file ******* ...

  2. vue小白必看的生命钩子函数图解

    还有3个钩子并未出现在图上: 1.activated生命周期钩子函数在keep-alive 组件激活时调用,该钩子在服务器端渲染期间不被调用. 2.deactivated生命周期钩子函数在keep-a ...

  3. Nginx status详解

    1. 启用nginx status配置 server {        listen *:80 default_server;        server_name _;        locatio ...

  4. Spark环境搭建(七)-----------spark的Local和standalone模式启动

    spark的启动方式有两种,一种单机模式(Local),另一种是多机器的集群模式(Standalone) Standalone 搭建: 准备:hadoop001,hadoop002两台安装spark的 ...

  5. 图解Raft之日志复制

    日志复制可以说是Raft集群的核心之一,保证了Raft数据的一致性,下面通过几张图片介绍Raft集群中日志复制的逻辑与流程: 在一个Raft集群中只有Leader节点能够接受客户端的请求,由Leade ...

  6. DevOps详解

    最近我阅读了很多有关DevOps的文章,其中一些非常有趣,然而一些内容也很欠考虑.貌似很多人越来越坚定地在DevOps与chef.puppet或Docker容器的熟练运用方面划了等号.对此我有不同看法 ...

  7. 如何实现文件上传 - JavaWeb

    直接上代码 ( idea 开发,SpringBoot 框架 ): 首先是Controller的写法: package com.xxx.Controller; import com.xxx.Tools. ...

  8. CSS面试细节整理(二)

    5.css盒模型: CSS 框模型 (Box Model) 规定了元素框处理元素内容.内边距.边框 和 外边距 的方式

  9. JS加密对应的c#解码

      escape不编码字符有69个:*,+,-,.,/,@,_,0-9,a-z,A-Z encodeURI不编码字符有82个:!,#,$,&,',(,),*,+,,,-,.,/,:,;,=,? ...

  10. QEMU KVM Libvirt手册(10): KVM的各种限制

    Overcommits KVM allows for both memory and disk space overcommit. However, hard errors resulting fro ...