ScheduledThreadPoolExecutor和时间轮算法比较

news2024/12/25 1:09:13

最近项目中需要用到超时操作,对于不是特别优秀的timer和DelayQueue没有看。

  • Timer 是单线程模式。如果某个 TimerTask 执行时间很久,会影响其他任务的调度。
  • Timer 的任务调度是基于系统绝对时间的,如果系统时间不正确,可能会出现问题。
  • TimerTask 如果执行出现异常,Timer 并不会捕获,会导致线程终止,其他任务永远不会执行。
  • 相比于 Timer,DelayQueue 只实现了任务管理的功能,需要与异步线程配合使用。

着重看了一下ScheduledThreadPoolExecutor和时间轮算法,以下一些大致讲解和我的理解。

ScheduledThreadPoolExecutor

前置知识

继承于ThreadPoolExecutor,也就是说调用线程执行任务的流程是一样的,详见ensurePrestart();方法。

在ScheduledThreadPoolExecutor中使用的阻塞队列是DelayedWorkQueue,一个自定义的类。内部使用以下queue存放定时任务。

private RunnableScheduledFuture<?>[] queue =
    new RunnableScheduledFuture<?>[INITIAL_CAPACITY];

上边提到的这个queue,是一个模拟优先级队列的堆(定时任务类实现了compareTo的方法),

    public int compareTo(Delayed other) {
            if (other == this) // compare zero if same object
                return 0;
            if (other instanceof ScheduledFutureTask) {
                ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
                long diff = time - x.time;
                if (diff < 0)
                    return -1;
                else if (diff > 0)
                    return 1;
                else if (sequenceNumber < x.sequenceNumber)
                    return -1;
                else
                    return 1;
            }
            long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
            return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
        }

查询时间复杂度log(n),可以从siftUp方法看出来是以何种规则把任务存放到数组。

大致流程

创建一个定时任务

把任务放在优先级阻塞队列

新增worker(ThreadPoolExecutor的流程)从阻塞队列拿出任务

判断任务时间(任务中会有预期执行时间,时间不到则调用available.awaitNanos(delay);阻塞)

由于是优先级队列,所以都从queue[0]去取任务,一个线程阻塞,其他线程available.await();也阻塞

具体代码:

    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);
                        if (delay <= 0L)
                            return finishPoll(first);
                        first = null; // don't retain ref while waiting
                        if (leader != null)
                            available.await();
                        else {
                            Thread thisThread = Thread.currentThread();
                            leader = thisThread;
                            try {
                                available.awaitNanos(delay);
                            } finally {
                                if (leader == thisThread)
                                    leader = null;
                            }
                        }
                    }
                }
            } finally {
                if (leader == null && queue[0] != null)
                    available.signal();
                lock.unlock();
            }
        }

    private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
            int s = --size;
            RunnableScheduledFuture<?> x = queue[s];
            queue[s] = null;
            if (s != 0)
                siftDown(0, x);
            setIndex(f, -1);
            return f;
        }

        public RunnableScheduledFuture<?> poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                RunnableScheduledFuture<?> first = queue[0];
                return (first == null || first.getDelay(NANOSECONDS) > 0)
                    ? null
                    : finishPoll(first);
            } finally {
                lock.unlock();
            }
        }

 个人看法

面对海量任务插入和删除的场景,会遇到比较严重的性能瓶颈

原因有以下两方面:

1. 自定义的阻塞队列queue不能自定义初始容量,只有16,需要考虑是否会频繁扩容

2. 就是上方提到的以下两个方法会导致 DelayedWorkQueue 数据结构频繁发生变化

    private void siftUp(int k, RunnableScheduledFuture<?> key) {
            while (k > 0) {
                int parent = (k - 1) >>> 1;
                RunnableScheduledFuture<?> e = queue[parent];
                if (key.compareTo(e) >= 0)
                    break;
                queue[k] = e;
                setIndex(e, k);
                k = parent;
            }
            queue[k] = key;
            setIndex(key, k);
        }

        /**
         * Sifts element added at top down to its heap-ordered spot.
         * Call only when holding lock.
         */
        private void siftDown(int k, RunnableScheduledFuture<?> key) {
            int half = size >>> 1;
            while (k < half) {
                int child = (k << 1) + 1;
                RunnableScheduledFuture<?> c = queue[child];
                int right = child + 1;
                if (right < size && c.compareTo(queue[right]) > 0)
                    c = queue[child = right];
                if (key.compareTo(c) <= 0)
                    break;
                queue[k] = c;
                setIndex(c, k);
                k = child;
            }
            queue[k] = key;
            setIndex(key, k);
        }

时间轮算法

原理

原理很简单,一张图直接概括(偷来的图,下图是netty的实现),只看原理不看类名

时间的维度是无限的,但是钟表却能表示无限的时间。所以这个时间轮算法和钟表相似都可以表示无限的时间,只要把圈数和槽位记录好即可。

需要注意的是,时间轮算法虽然看起来是物理上的⚪,但其实只是逻辑上的回环,走到末尾时重新回到头。

类比钟表,时间轮上也有一个个的插槽(slot),一个插槽代表的是时间间隔(interval),时间间隔越小,时间表示越精确。

大致流程

放入定时任务

封装任务记录剩余圈数,槽位等等

扫描槽位以及链表,扫描到对应槽位找到剩余圈数为0的执行

个人看法,因为这只是算法思想,所以以下不是缺陷而是需要注意的点

  • 如果长时间没有到期任务,那么会存在时间轮空推进的现象。
  • 只适用于处理耗时较短的任务,由于 Worker 是单线程的,如果一个任务执行的时间过长,会造成 Worker 线程阻塞。
  • 相比传统定时器的实现方式,内存占用较大。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1871348.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安装Django Web框架

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 Django是基于Python的重量级开源Web框架。Django拥有高度定制的ORM和大量的API&#xff0c;简单灵活的视图编写&#xff0c;优雅的URL&#xff0c;适…

SyncFolders文件备份—办公人员必备

SyncFolders支持在两个或多个文件夹之间同步文件&#xff0c;用户可以将重要文件同步到多个位置&#xff0c;如备份硬盘、网络共享文件夹或云存储等。通过设定同步规则&#xff0c;可以自动备份和同步更新&#xff0c;减少手动操作的繁琐&#xff0c;确保文件的安全和可访问性。…

C++编程(五)单例模式 友元

文章目录 一、单例模式&#xff08;一&#xff09;概念&#xff08;二&#xff09;实现方式1. 饿汉式2. 懒汉式 二、友元&#xff08;一&#xff09;概念&#xff08;二&#xff09;友元函数1.概念2.语法格式3. 使用示例访问静态成员变量访问非静态成员变量 &#xff08;三&…

vue3集成Echarts,自定义ToolBox时无法监听参数变化

问题描述 在vue中集成echart中自定义ToolBox工具的点击事件&#xff0c;并将里面的typeBar赋给data中的变量&#xff0c;随后用watch监听这个变量的变化&#xff0c;发现监听不到&#xff01;&#xff01;&#xff01;&#xff01; 解决方案&#xff1a;一直以为是watch不能监…

接口自动化测试框架实战(Pytest+Allure+Excel)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1. Allure 简介 Allure 框架是一个灵活的、轻量级的、支持多语言的测试报告工具&#xff0c;它不…

java根据枚举值动态获取对象bean调用方法示例

1.创建操作枚举 Getter AllArgsConstructor public enum CustomerOperatorType {/*** 用户操作记录分类*/ACCOUNT("账户冻结/解冻"),LEVEL("等级记录"),SCORE( "积分记录");private final String desc; } 2.公共基础类 import lombok.AllAr…

word图题表题公式按照章节编号(不用题注)

预期效果&#xff1a; 其中3表示第三章&#xff0c;4表示第3章里的第4个图。标题、公式编号也是类似的。 为了达到这种按照章节编号的效果&#xff0c;原本可以用插入题注里的“包含章节编号” 但实际情况是&#xff0c;这不仅需要一级标题的序号是用“开始->多级列表”自动…

编码注入

Url&#xff1a;http://www.xxxxxxxx/newsdetail.php?idMjgxOA 判断参数Id存在数字型注入,试了报错注入不行&#xff0c;只能去盲注了 验证Poc1&#xff1a;idMTg4OS8x 等同于&#xff1a;id1889/1 poc2&#xff1a;idMTg4OS8w 等同于&#xff1a;id1889/0 /1 /0 用asci…

数据结构与算法笔记:高级篇 - B+树:MySql数据库索引是如何实现的?

概述 作为一名软件开发工程师&#xff0c;你对数据库肯定再熟悉不过了。MySQL 作为主流的数据库存储系统&#xff0c;它在我们的业务开发中&#xff0c;有着举足轻重的地位。在工作中&#xff0c;为了加速数据库中数据的查找速度&#xff0c;我们常用的处理思路是&#xff0c;…

Java基础教程:入门篇Java基础概念Java入门实例

学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……&#xff09; 2、学会Oracle数据库入门到入土用法(创作中……&#xff09; 3、手把手教你开发炫酷的vbs脚本制作(完善中……&#xff09; 4、牛逼哄哄的 IDEA编程利器技巧(编写中……&#xff09; 5、面经吐血整理的 面试技…

Acitity跳转延时10s会导致anr的时序问题讨论

背景&#xff1a; 针对前些天直播时候&#xff0c;主要讲解是launcher启动app&#xff0c;Activity onResume延时10s不会anr&#xff0c;但是Activity内部activity1跳转activity2就会anr问题展开了讨论 https://mp.weixin.qq.com/s/_uA5yKUTUw-9H-tWxGNK9g 这个原因为啥已经在…

Tomcat WEB站点部署

目录 1、使用war包部署web站点 2、自定义默认网站目录 3、部署开源站点&#xff08;jspgou商城&#xff09; 对主机192.168.226.22操作 对主机192.168.226.20操作 上线的代码有两种方式&#xff1a; 第一种方式是直接将程序目录放在webapps目录下面&#xff0c;这种方式…

昇思25天学习打卡营第03天|张量Tensor

何为张量&#xff1f; 张量&#xff08;Tensor&#xff09;是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数&#xff0c;这些线性关系的基本例子有内积、外积、线性映射以及笛卡儿积。其坐标在 &#x1d45b;维空间内&#xff0c;有  &#x1d45b;&a…

【LeetCode】六、哈希集合Set相关:存在重复元素判断 + MyHashSet设计

文章目录 1、Set集合2、Java中的Set集合3、leetcode217&#xff1a;存在重复元素4、P705&#xff1a;设计哈希集合 1、Set集合 无序&#xff1a;写进去的顺序和遍历读出来的顺序不一样不重复 主要作用&#xff1a;查看是否有重复元素&#xff08;给一个数组&#xff0c;转为…

WITS核心价值观【创新】篇|从财务中来,到业务中去

「客尊」、「诚信」、「创新」 与「卓越」 是纬创软件的核心价值观。我们秉持诚信态度&#xff0c;致力于成为客户长期且值得信赖的合作伙伴。持续提升服务厚度&#xff0c;透过数字创新实践多市场的跨境交付&#xff0c;助客户保持市场领先地位。以追求卓越的不懈精神&#xf…

Visual Studio 工具使用 之 即时窗口

即时窗口&#xff1a;是Visual Studio中的一个调试工具&#xff0c;它允许开发人员在调试过程中执行代码并查看结果。开发人员可以在即时窗口中输入和执行表达式、调用方法&#xff0c;并查看变量的值。即时窗口通常用于调试过程中的快速测试和验证代码的正确性。 就是下面的这…

[leetcode]圆圈中最后剩下的数字/ 破冰游戏

. - 力扣&#xff08;LeetCode&#xff09; class Solution {int f(int num, int target) {if (num 1) {return 0;}int x f(num - 1, target);return (target x) % num;} public:int iceBreakingGame(int num, int target) {return f(num, target);} };

编程入门指南

一、了解编程与编程语言 编程&#xff1a;编程是使计算机按照人类编写的指令进行工作的过程。这些指令被编写成计算机可以理解的代码&#xff0c;称为程序。编程语言&#xff1a;编程语言是人与计算机交流的工具。常见的编程语言有Python、Java、C、JavaScript等。 二、选择编…

继续捡钱,每天几百块!

每日操作计划&#xff1a; 标普信息科技(161128)&#xff0c;溢价8.5%&#xff0c;限购100&#xff0c;一拖七&#xff0c;单户每天700*8.5%59元 印度基金LOF(164824)&#xff0c;溢价2.6%&#xff0c;限购100&#xff0c;一拖七&#xff0c;单户每天700*2.6%18元 美元债LOF(…

C语言笔记26 •顺序表应用•

基于动态顺序表实现通讯录项目 1.通讯录其实也就是顺序表&#xff0c;就是把里面存的数据类型变了一下 &#xff0c;所以有一些方法对于顺序表适用&#xff0c;对于通讯录也是适用的&#xff08;初始化&#xff0c;销毁&#xff0c;内存空间扩容&#xff09;。 2.要用到顺序表…