java线程池系列(3)-ScheduledThreadPoolExecutor

来源:互联网 发布:剑三捏脸数据喵哥成男 编辑:程序博客网 时间:2024/06/07 13:45

前言

定时执行任务是非常常见的需求,比如我们通常会使用spring或者quartz来实现定时任务,JDK本身也提供了多线程并发执行定时任务的框架,即ScheduledThreadPoolExecutor,它是基于优先级队列和ThreadPoolExecutor线程池技术来实现的,本篇介绍其实现的原理。如果你还不了解ThreadPoolExecutor,建议先行学习 ThreadPoolExecutor实现原理 。


优先级队列

ScheduledThreadPoolExecutor由于可以延时执行任务,甚至定时执行任务,其底层需要一个强大的队列来支撑,DelayedWorkQueue就是一个满足需求的优先级队列。

DelayedWorkQueue使用了二叉堆数据结构,内部使用一个数组来存储节点 ,二叉堆可以看作是近似的完全二叉树,二叉堆需要满足如下两个条件:

1、父结点的值总是>=任何一个子节点的值(最大堆),或者是父结点的值总是<=任何一个子节点的值(最小堆)。

2、每个结点的左子树和右子树都是一个二叉堆。

下面举一个最小二叉堆的例子,可以看到,除了h=4最后一层,其它层均为完全二叉树,节点总是从左往右填满,存储的时候,一层一层,从左往右存储,如下存储到数组中为[1,2,3,4,5,6,7,8,9,10]。

这里写图片描述


每当获取节点的时候,总是获取第一个节点,即内部数组结构索引为0的节点,获取后会破坏堆的结构,所以必须有新的节点来替代索引为0的位置,这时候需要使用一定的算法,如果是新增一个节点,节点会先追加到内部数组最后面,然后使用算法,确保添加后仍然保持堆结构。有兴趣可以查阅源码,并不是很复杂。

ScheduledThreadPoolExecutor实现细节

前面简单介绍了DelayedWorkQueue,在具体实现中,队列任务的类型为RunnableScheduledFuture,在使用时,任务最重要实现如下两个方法:

/**** 任务还需要多少时间才需要执行**/long getDelay(TimeUnit unit);/**** 用于比较,实现使用的是最小堆,决定在数组中的位置**/public int compareTo(T o);



对于一个RunnableScheduledFuture,最重要的两个属性为: time 和 period,time表示下次什么时候执行任务(想想我们可以延迟执行任务),period表示每隔多长时间执行一次任务(想想我们可以定时执行任务),period=0表示任务只执行一次。

我们从一个高的视角来看整个设计:

这里写图片描述

1、 优先队列中有两个重要的属性,lock和leader,lock是个排他锁,用于控制所有线程获取、删除、添加任务的并发操作,leader用于指向当前哪个线程正在等待第一个任务到期执行,如果有线程获取到锁,欲等待获取任务,但此时leader!=null,这时候,当前线程只能进入available条件等待状态,这样可以更加高效控制多个线程获取任务。这里要注意的是,如果leader!=null,但是这时候添加了一个新的任务,而且此任务加到索引为0的位置上了,也就是最先执行,这时候添加任务的线程会把leader置为null,因为最先执行的任务已经发生了变化,释放leader后其它线程才有机会执行最新那个任务。

2、创建 ScheduledThreadPoolExecutor 的时候,需要提供一个 corePoolSize参数,用于初始化ThreadPoolExecutor,所以当前如果线程数<=corePoolSize,那么每新增一个任务,都会创建一个新的工作线程,更多细节可以参考 ThreadPoolExecutor实现原理 。

3、leader属性是一个特别的存在,它用于表示谁在等待获取第一个任务,当有其他线程也需要等待第一个任务时,这时候需要直接进入条件等待(available条件),而不是并发等待同一个任务。当为leader的线程获取到了任务,如果还有其它任务,就会唤醒条件等待的线程竞争leader。

4、当执行完一个任务的时候,如果为定时任务,这时候,需要重新计算下次执行的时间,然后重新添加到优先级队列中。这样来实现定时执行,这里要注意时间的计算规则,计算方式如下所示:

private void setNextRunTime() {            long p = period;            if (p > 0)                time += p;            else                time = triggerTime(-p);        }

如果定时执行的周期时间 period>>0,则下次执行时间 time += period,这里time为上一次执行的时间,从这里可以看出,间隔时间把任务的执行时间也计算进去了,比方说,一个任务,执行需要2S,每3S执行一次,真正执行时,真的就是3S出来一次结果,而不会受到任务需要执行2S的影响。


总结

了解DelayedWorkQueue的工作方式和ThreadPoolExecutor的原理是学习 ScheduledThreadPoolExecutor 的前提条件,ScheduledThreadPoolExecutor只是基于此作了比较简单的封装。如果觉得DelayedWorkQueue不好理解,可以先学习PriorityQueue(基于堆实现的优先队列)和DelayQueue(基于PriorityQueue实现的延时队列)。

到这里,如果你发现要实现高效的定时任务,又无法使用spring或者其它第三方框架,那么ScheduledThreadPoolExecutor绝对是首选方案。

0 0
原创粉丝点击