死磕 java线程系列之线程池深入解析——定时任务执行流程 (2)

再来看看reExecutePeriodic()方法。

void reExecutePeriodic(RunnableScheduledFuture<?> task) { // 线程池状态检查 if (canRunInCurrentRunState(true)) { // 再次把任务扔到任务队列中 super.getQueue().add(task); // 再次检查线程池状态 if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else // 保证工作线程足够 ensurePrestart(); } }

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

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

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

DelayedWorkQueue内部类

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

说了这么多,它到底是怎么实现的呢?

其实,延时队列我们在前面都详细分析过,想看完整源码分析的可以看看之前的《死磕 java集合之DelayQueue源码分析》。

延时队列内部是使用“堆”这种数据结构来实现的,有兴趣的同学可以看看之前的《拜托,面试别再问我堆(排序)了!》。

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

public RunnableScheduledFuture<?> take() throws InterruptedException { final ReentrantLock lock = this.lock; // 加锁 lock.lockInterruptibly(); try { for (;;) { // 堆顶任务 RunnableScheduledFuture<?> first = queue[0]; // 如果队列为空,则等待 if (first == null) available.await(); else { // 还有多久到时间 long delay = first.getDelay(NANOSECONDS); // 如果小于等于0,说明这个任务到时间了,可以从队列中出队了 if (delay <= 0) // 出队,然后堆化 return finishPoll(first); // 还没到时间 first = null; // 如果前面有线程在等待,直接进入等待 if (leader != null) available.await(); else { // 当前线程作为leader Thread thisThread = Thread.currentThread(); leader = thisThread; try { // 等待上面计算的延时时间,再自动唤醒 available.awaitNanos(delay); } finally { // 唤醒后再次获得锁后把leader再置空 if (leader == thisThread) leader = null; } } } } } finally { if (leader == null && queue[0] != null) // 相当于唤醒下一个等待的任务 available.signal(); // 解锁,本文由公从号“彤哥读源码”原创 lock.unlock(); } }

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

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

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

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

其它

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

public void execute(Runnable command) { schedule(command, 0, NANOSECONDS); } public <T> Future<T> submit(Callable<T> task) { return schedule(task, 0, NANOSECONDS); } 总结

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

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

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

彩蛋

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

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

答案是肯定的,下一章我们一起来看看这种基于“工作窃取”理论的线程池——ForkJoinPool。

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpwgyd.html