线程池深入解析——定时任务执行流程

  注:java源码分析部分如无特殊说明均基于 java8 版本。

  注:本文基于ScheduledThreadPoolExecutor定时线程池类。

  简介

  前面我们一起学习了普通任务、未来任务的执行流程,今天我们再来学习一种新的任务——定时任务。

  定时任务是我们经常会用到的一种任务,它表示在未来某个时刻执行,或者未来按照某种规则重复执行的任务。

  问题

  (1)如何保证任务是在未来某个时刻才被执行?

  (2)如何保证任务按照某种规则重复执行?

  来个栗子

  创建一个定时线程池,用它来跑四种不同的定时任务。

  定时任务总体分为四种:

  (1)未来执行一次的任务,无返回值;

  (2)未来执行一次的任务,有返回值;

  (3)未来按固定频率重复执行的任务;

  (4)未来按固定延时重复执行的任务;

  本文主要以第三种为例进行源码解析。

  scheduleAtFixedRate()方法

  提交一个按固定频率执行的任务。

  可以看到,这里的处理跟未来任务类似,都是装饰成另一个任务,再拿去执行,不同的是这里交给了delayedExecute()方法去执行,这个方法是干嘛的呢?

delayedExecute()方法

延时执行。

  到这里就结束了?!

  实际上,这里只是控制任务能不能被执行,真正执行任务的地方在任务的run()方法中。

  还记得上面的任务被装饰成了ScheduledFutureTask类的实例吗?所以,我们只要看ScheduledFutureTask的run()方法就可以了。

  ScheduledFutureTask类的run()方法

  定时任务执行的地方。

  可以看到,对于重复性任务,先调用FutureTask的runAndReset()方法,再设置下次执行的时间,最后再调用reExecutePeriodic()方法。

  FutureTask的runAndReset()方法与run()方法类似,只是其任务运行完毕后不会把状态修改为NORMAL,有兴趣的同学点进源码看看。

  再来看看reExecutePeriodic()方法。

  到这里是不是豁然开朗了,原来定时线程池执行重复任务是在任务执行完毕后,又把任务扔回了任务队列中。

  重复性的问题解决了,那么,它是怎么控制任务在某个时刻执行的呢?

  OK,这就轮到我们的延时队列登场了。

  DelayedWorkQueue内部类

  我们知道,线程池执行任务时需要从任务队列中拿任务,而普通的任务队列,如果里面有任务就直接拿出来了,但是延时队列不一样,它里面的任务,如果没有到时间也是拿不出来的,这也是前面分析中一上来就把任务扔进队列且创建Worker没有传入firstTask的原因。

  我们这里只拿一个take()方法出来分析。

  大致的原理是,利用堆的特性获取最快到时间的任务,即堆顶的任务:

  (1)如果堆顶的任务到时间了,就让它从队列中了队;

  (2)如果堆顶的任务还没到时间,就看它还有多久到时间,利用条件锁等待这段时间,待时间到了后重新走(1)的判断;

  这样就解决了可以在指定时间后执行任务。

  其它

  其实,ScheduledThreadPoolExecutor也是可以使用execute()或者submit()提交任务的,只不过它们会被当成0延时的任务来执行一次。

  总结

  实现定时任务有两个问题要解决,分别是指定未来某个时刻执行任务、重复执行。

  (1)指定某个时刻执行任务,是通过延时队列的特性来解决的;

  (2)重复执行,是通过在任务执行后再次把任务加入到队列中来解决的。

  彩蛋

  到这里基本上普通的线程池的源码解析就结束了,这种线程池是比较经典的实现方式,整体上来说,效率相对不是特别高,因为所有的工作线程共用同一个队列,每次从队列中取任务都要加锁解锁操作。

  那么,能不能给每个工作线程配备一个任务队列呢,在提交任务的时候就把任务分配给指定的工作线程,这样在取任务的时候就不需要频繁的加锁解锁了。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章