简介
上一篇文章剖析了ThreadPoolExecutor的原理和实现,这里剖析下ScheduledThreadPoolExecutor的实现方式。ScheduledThreadPoolExecuto能够执行延迟固定时间的任务和周期性任务。在需要多工作线程的情况下,它比Timer更方便,同时比ThreadPoolExecutor有更大灵活性和能力。
但是在使用ScheduledThreadPoolExecutor时,有几个点必须注意:
1.在执行延迟任务时,任务的执行不会早于你设定的延迟时间,但是也不保证恰好在达到你设定的延迟时间时就马上执行。在同一时间开始执行的任务,按照FIFO顺序提交给线程。
2.虽然ScheduledThreadPoolExecutor继承了ThreadPoolExecutor。但是针对ThreadPoolExecutor的一些性能调优的方法不一定试用于ScheduledThreadPoolExecutor。在ThreadPoolExecutor中,调整corePoolSize,maximumPoolSize两个参数可以控制任务的执行速度,例如当前线程小于corePoolSize时,可以马上为新任务创建线程而不必等待,当前线程大于corePoolSize小于maximumPoolSize。可以动态扩展当前线程以提高并发速度等。但是对于ScheduledThreadPoolExecutor,它实际上是一个固定线程数,无界阻塞队列的线程池,所有任务都是先放到工作队列中,而不会有直接为其创建线程的优待。所以调整corePoolSize,maximumPoolSize对ScheduledThreadPoolExecutor来说,性能上不会有影响。
实现原理
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,它先把用户提交的任务进一步封装成ScheduledFutureTask。再把ScheduledFutureTask放到任务队列中,交给线程池的线程去执行。可以说ScheduledThreadPoolExecutor与ThreadPoolExecutor的最大区别就是多了一步封装ScheduledFutureTask的过程。其他像任务队列的执行,线程的管理等都于ThreadPoolExecutor差不多,甚至就是直接使用了ThreadPoolExecutor的方法。下面就主要研究下ScheduledThreadPoolExecutor与ThreadPoolExecutor的不同地方。与ThreadPoolExecutor相关的这里直接省略,需要参考ThreadPoolExecutor的地方,请移步ThreadPoolExecutor的原理和实现。
ScheduledFutureTask的设计
把用户任务进一步封装成ScheduledFutureTask是ScheduledThreadPoolExecutor与ThreadPoolExecutor的主要区别。因此ScheduledFutureTask的设计就是ScheduledThreadPoolExecutor的核心。ScheduledFutureTask是ScheduledThreadPoolExecutor的一个内部类。ScheduledFutureTask继承了FutureTask。先看下ScheduledFutureTask独有的属性:
属性 | 属性描述 |
long sequenceNumber | 跳出FIFO调度关系的序列号 |
long time | 激活任务的时间,单位是纳秒。注意:这个时间不是需要延迟的时间。而是用纳秒表示的自然时间。 |
long period | 周期性任务的时间间隔,单位是纳秒。正数表示固定频率,负数表示固定的延迟时间。 |
ScheduledFutureTask提供了三种构造方法:
1.ScheduledFutureTask(Runnable r, V result, long ns)。 创建设定了任务执行的触发时间的一次性任务
1.ScheduledFutureTask(Runnable r, V result, long ns, long period)。创建设定了任务触发时间和周期时间的周期性任务。
2.ScheduledFutureTask(Callable<V> callable, long ns)。创建Callable,固定触发时间的一次性任务。
ScheduledFutureTask继承自FutureTask,所以需要实现自己的run方法:
public void run() { if (isPeriodic()) runPeriodic(); else ScheduledFutureTask.super.run(); }
如果是一次性任务,则执行FutureTask的run方法,如果是周期性任务,则执行runPeriodic()方法。下面看下runPeriodic()做了什么事情:
private void runPeriodic() { boolean ok = ScheduledFutureTask.super.runAndReset(); boolean down = isShutdown(); // Reschedule if not cancelled and not shutdown or policy allows if (ok && (!down || (getContinueExistingPeriodicTasksAfterShutdownPolicy() && !isStopped()))) { long p = period; if (p > 0) time += p; else time = triggerTime(-p); ScheduledThreadPoolExecutor.super.getQueue().add(this); } // This might have been the final executed delayed // task. Wake up threads to check. else if (down) interruptIdleWorkers(); }
首先,先执行FutureTask的runAndReset()。这个方法尝试执行一次用户任务,重置状态,并返回是否成功执行。判断下状态:任务成功执行了,线程池没有shutdown,任务没有被取消。那么设置任务的下一次执行时间。这里设置下次执行时间比较有意思如果period>0则time=time+p,若果period<=0,time=now()+p。即前者是上次任务开始的时间加上时间周期,后置是上一次任务执行完成后的时间加上时间周期。这样说有点拗口,换种方式来说:当把时间周期设置为正数,那么表示该任务每多长时间执行一次,任务的执行频率是固定的。如果间周期设置为负数,表示前一次任务执行完之后间隔多长时间开始执行下一次任务,每个任务之间的延时是固定的。
ScheduledThreadPoolExecutor的实现
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor并且实现了ScheduledExecutorService接口。
ScheduledExecutorService定义了使用ScheduledThreadPoolExecutor提交任务的接口schedule。schedule是一组返回值和参数不同的overwrite接口。以供用户选择提交任务的模式,比如是一次性的定时任务还是周期性的任务,需不需要返回结果等等。不管是哪个schedule接口,他的实现功能主要是把用户提交的任务和模式封装成ScheduledFutureTask,然后调用delayedExecute(Runnable command)。所以delayedExecute方法是真正的处理任务的方法,代码如下:
private void delayedExecute(Runnable command) { if (isShutdown()) { reject(command); return; } // Prestart a thread if necessary. We cannot prestart it // running the task because the task (probably) shouldn't be // run yet, so thread will just idle until delay elapses. if (getPoolSize() < getCorePoolSize()) prestartCoreThread(); super.getQueue().add(command); }
如果线程池shutdown了,则执行reject策略。如果当前线程小于核心线程,那么调用prestartCoreThread()创建一个线程线程。prestartCoreThread()实际上是调用了ThreadPoolExecutor类里的addIfUnderCorePoolSize(null)。如果你看了ThreadPoolExecutor的分析。就会知道这个addIfUnderCorePoolSize(null)的参数设置为null其实就是只创建线程,而不会把当前任务直接提交给这个线程。任务直接通过super.getQueue().add(command);放到任务队列里去了。这也解释了前面简介中第2点说了为什么调整corePoolSize也不会让任务马上执行了,不管你corePoolSize调多大,你的任务还是需要排队,只是你的线程多了,可能任务排队等待的时间少了。
另外这里也可以解释简介中第1点注意事项:对于定时任务,只能保证执行时间不会早于你设定的时间。但不一定会恰好在你设定的时间点上执行。因为你提交的任务都是排队的。加入你12点00分00秒提交了一个任务,希望这个任务在12点00分05秒的时刻执行。加入你的线程任务处理的足够快,在12点00分03秒的时候这个任务得到了一个线程,那么这个线程开始执行你的任务,发现你的执行时间是12点00分05秒,那么线程会等2秒到12点00分05秒的时候开始执行你的任务;但是如果线程池线程数比较少或者任务执行比较耗时等,可能使你的任务在12点00分10秒的时刻才分配到了一个线程,然后线程发现你的任务已经过了到了执行时间,马上开始执行你的任务,这种情况下,相当于任务比你设定的时间延迟了5秒。
相关推荐
Java并发编程:线程池的使用 - 平凡希 - 博客园平凡希博客园首页联系管理随笔 - 127 文章 - 1 评论 - 94Java并发编程:线程池的使用在前面
23 高并发编程和线程池,教程视频:java中高并发编程和线程池
并发编程之Executor线程池原理与源码解读.pdf
在当今计算机软件开发领域,多核处理器的广泛应用使得并发编程成为开发者必备的技能之一。而了解 Java 并发编程以及其中的 JUC(java.util.concurrent)线程池,对于构建高性能、高可伸缩性的应用程序具有重要意义。...
课程内容包括了JAVA手写线程池,UC线程池API详解,线程安全根因详解,锁与原子类,分布式锁原理与实现方式,并发编程-AQS等等针对性非常强的JAVA编程开发教程,这其中的内容对JAVA开发技能的拔尖,非常的有帮助。...
并发编程之线程与线程池.pptx 内部培训PPT 多线程 线程池
ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的...
下面小编就为大家带来一篇java并发编程_线程池的使用方法(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
│ 高并发编程第一阶段35讲、线程池原理与自定义线程池.mp4 │ 高并发编程第一阶段36讲、自定义个简单的线程池并且测试.mp4 │ 高并发编程第一阶段37讲、给线程池增加拒绝策略以及停止方法.mp4 │ 高并发编程第...
并发编程之深入理解JMM&并发三大特性
《Java并发编程的艺术》内容涵盖Java并发编程机制的底层实现原理、Java内存模型、Java并发编程基础、Java中的锁、并发容器和框架、原子类、并发工具类、线程池、Executor框架等主题,每个主题都做了深入的讲解,同时...
课程内容包括了JAVA手写线程池,UC线程池API详解,线程安全根因详解,锁与原子类,分布式锁原理与实现方式,并发编程-AQS等等针对性非常强的JAVA编程开发教程,这其中的内容对JAVA开发技能的拔尖,非常的有帮助。...
深入理解高并发编程-Java线程池核心技术 全面详解Java线程池核心技术
java并发编程相关总结
5.4 并发线程池底层原理及实现详解(1).mp4
5.4 并发线程池底层原理及实现详解(2).mp4
Java高并发编程,构建并发编程知识体系,提升面试成功率,完整版17章视频教程下载。 本课程将结合大量图示及代码演示,带你掌握多线程并发编程(线程安全,线程调度,线程封闭,同步容器等)与高并发处理思路与手段...
第2章介绍Java并发编程的底层实现原理,介绍在CPU和JVM这个层面是如何帮助Java实现并发编程的。 第3章介绍深入介绍了Java的内存模型。Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本...
JAVA并发编程艺术 高清pdf : 1.并发变成的挑战 2. java并发机制的底层实现原理 3. java 内存模型 4. java并发编程基础 5.java中的锁。。。。。。。
java并发编程pdf文档第二部分:Java并发编程实战.pdf、Java多线程编程核心技术.pdf、实战Java高并发程序设计.pdf