当前位置: 首页 > 后端技术 > Java

Netty系列:HashedWheelTimer,一个定时器的高效实现

时间:2023-04-01 20:42:44 Java

介绍Timer是实际应用中非常常用和有效的工具。它的原理是将要执行的任务按照执行时间的先后顺序排序,然后在特定的时间执行。JAVA提供了java.util.Timer、java.util.concurrent.ScheduledThreadPoolExecutor等多种Timer工具,但是这些工具在执行效率上还是存在一些缺陷,所以netty提供了HashedWheelTimer,一个优化的Timer类。下面我们来看看netty的Timer有什么区别。java.util.TimerTimer是JAVA在1.3引入的。所有任务都存储在里面的TaskQueue中:privatefinalTaskQueuequeue=newTaskQueue();TaskQueue底层是一个TimerTask数组,用来存放要执行的任务。私人TimerTask[]queue=newTimerTask[128];看起来TimerTask只是一个数组,但是Timer让这个队列变成了一个平衡的二叉堆。添加TimerTask时,会插入到Queue的尾部,然后调用fixup方法重新平衡:voidadd(TimerTasktask){//Growbackingstoreif(size+1==queue.length)queue=Arrays.copyOf(queue,2*queue.length);队列[++大小]=任务;修正(大小);当正在运行的任务从堆中移除时,将调用fixDown方法进行重新平衡:voidremoveMin(){queue[1]=queue[size];队列[大小--]=null;//删除额外的引用以防止内存泄漏fixDown(1);fixup的原理是比较当前节点和它的父节点,如果比父节点小,就和父节点交互,然后遍历这个过程:privatevoidfixUp(intk){while(k>1){整数j=k>>1;如果(队列[j].nextExecutionTime<=队列[k].nextExecutionTime)中断;TimerTasktmp=queue[j];队列[j]=队列[k];队列[k]=tmp;k=j;fixDown的原理是比较当前节点与其子节点,如果当前节点大于子节点,则降级:privatevoidfixDown(intk){intj;而((j=k<<1)<=大小&&j>0){如果(j<大小&&queue[j].nextExecutionTime>queue[j+1].nextExecutionTime)j++;//j索引最小的孩子if(queue[k].nextExecutionTime<=queue[j].nextExecutionTime)break;TimerTasktmp=queue[j];队列[j]=队列[k];队列[k]=tmp;k=j;文章。java.util.concurrent.ScheduledThreadPoolExecutor虽然Timer很好用,而且是线程安全的,但是对于Timer来说,如果要提交一个任务,需要创建一个TimerTask类来封装具体的任务,不是很通用。所以JDK在5.0引入了更通用的ScheduledThreadPoolExecutor,它是一个线程池,使用多个线程来执行特定的任务。当线程池的线程数等于1时,ScheduledThreadPoolExecutor相当于Timer。ScheduledThreadPoolExecutor中保存的任务是一个DelayedWorkQueue。DelayedWorkQueue与DelayQueue和PriorityQueue一样,都是基于堆的数据结构。因为堆需要不断的进行siftUp和siftDown重平衡操作,所以它的时间复杂度是O(logn)。下面是DelayedWorkQueue的shiftUp和siftDown的实际代码:privatevoidsiftUp(intk,RunnableScheduledFuturekey){while(k>0){intparent=(k-1)>>>1;RunnableScheduledFuturee=queue[parent];如果(key.compareTo(e)>=0)中断;队列[k]=e;设置索引(e,k);k=父母;}队列[k]=键;设置索引(键,k);}privatevoidsiftDown(intk,RunnableScheduledFuturekey){inthalf=size>>>1;while(kc=queue[child];intright=child+1;if(right0)c=queue[child=right];如果(key.compareTo(c)<=0)中断;队列[k]=c;设置索引(c,k);k=孩子;}队列[k]=键;两者都是基于堆结构。ScheduledThreadPoolExecutor虽然改进了Timer,但是两者的效率差不多。那么有没有更高效的方法呢?例如,O(1)是否可以实现?我们知道Hash可以实现高效的O(1)查找。想象一下,如果我们有一个无限刻度的时钟,然后将要执行的任务按照间隔时间的顺序分配给这些刻度。每当时钟移动一个刻度,即可以在这个刻度上执行相应的任务,如下图所示:这种算法称为SimpleTimingWheel算法。但是这个算法是一个理论算法,因为不可能给所有的区间长度都分配相应的尺度。这会消耗大量的无效内存空间。所以我们可以折衷一下,先用hash处理区间的长度。这样就可以缩短区间的底数,如下图所示:本例中我们选择8作为底数,将区间除以8,余数作为hash位置,商为用作节点的值。每遍历一次poll,节点的值就减一。当该节点的值为0时,表示该节点可以取出来执行。该算法称为HashedWheelTimer。Netty提供了该算法的实现:publicclassHashedWheelTimerimplementsTimerHashedWheelTimer使用HashedWheelBucket数组存储具体的TimerTask:privatefinalHashedWheelBucket[]wheel;先看创建wheel的方法:privatestaticHashedWheelBucket[]createWheel(intticksPerWheel){//ticksPerWheel不得大于2^30checkInRange(ticksPerWheel,1,1073741824,"ticksPerWheel");ticksPerWheel=normalizeTicksPerWheel(ticksPerWheel);HashedWheelBucket[]wheel=newHashedWheelBucket[ticksPerWheel]=;因为我