netty-HashedWheelTimer源码解析

news2024/12/24 0:31:02

一、案例分析

    public void testExecutionOnTaskExecutor() throws InterruptedException {
        int timeout = 10;

        final CountDownLatch latch = new CountDownLatch(1);
        final CountDownLatch timeoutLatch = new CountDownLatch(1);
        Executor executor = new Executor() {
            @Override
            public void execute(Runnable command) {
                try {
                    command.run();
                } finally {
                    latch.countDown();
                }
            }
        };
        final HashedWheelTimer timer = new HashedWheelTimer(Executors.defaultThreadFactory(), 100,
                TimeUnit.MILLISECONDS, 32, true, 2, executor);
        timer.newTimeout(new TimerTask() {
            @Override
            public void run(final Timeout timeout) throws Exception {
                timeoutLatch.countDown();
            }
        }, timeout, TimeUnit.MILLISECONDS);

        latch.await();
        timeoutLatch.await();
        timer.stop();
    }

二、底层接口

public interface Timer {

    /**
     * Schedules the specified {@link TimerTask} for one-time execution after
     * the specified delay.
     *
     * @return a handle which is associated with the specified task
     *
     * @throws IllegalStateException       if this timer has been {@linkplain #stop() stopped} already
     * @throws RejectedExecutionException if the pending timeouts are too many and creating new timeout
     *                                    can cause instability in the system.
     */
    Timeout newTimeout(TimerTask task, long delay, TimeUnit unit);

    /**
     * Releases all resources acquired by this {@link Timer} and cancels all
     * tasks which were scheduled but not executed yet.
     *
     * @return the handles associated with the tasks which were canceled by
     *         this method
     */
    Set<Timeout> stop();
}

 newTimeOut: 提交任务,并在延迟delay的unit单位时间后,执行任务

stop:取消所有调度中但是未执行的任务

三、构建hashWheel函数

public HashedWheelTimer(
        ThreadFactory threadFactory,
        long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
        long maxPendingTimeouts, Executor taskExecutor)

threadFactory:线程工厂,用于创建工作线程

tickDuration:每次转动的间隔的时间

unit:每次转动的间隔的时间的时间单元

ticksPerWheel:时间轮上一共有多少个 slot,默认 512 个。分配的 slot 越多,占用的内存空间就越大

leakDetection:是否开启内存泄漏检测

maxPendingTimeouts:最大等待任务数

taskExecutor:任务处理器

3.1 HashedWheelBucket[] wheel

 // 时间轮的环形数组
 wheel = createWheel(ticksPerWheel);

private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
        //ticksPerWheel may not be greater than 2^30
        checkInRange(ticksPerWheel, 1, 1073741824, "ticksPerWheel");

        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i ++) {
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
}
private static final class HashedWheelBucket {
        // Used for the linked-list datastructure
        private HashedWheelTimeout head;
        private HashedWheelTimeout tail;
。。。。
}

通过构造方法可以得知,其实这个类似于HashMap底层是数组+链表的结构,构造出了一个2^n的数组,位数计算是效率最高的算法

一直左移1位,即相当于*2,即该代码是为了得出 >= ticksPerWheel的2^n值,如ticksPerWheel
为5,则结果值为8
    private static int normalizeTicksPerWheel(int ticksPerWheel) {
        int normalizedTicksPerWheel = 1;
        while (normalizedTicksPerWheel < ticksPerWheel) {
            normalizedTicksPerWheel <<= 1;
        }
        return normalizedTicksPerWheel;
    }

得出2^n次大小的数组后,即可通过[(2^n)-1] & index 进行求模,所以记录一个值

mask = wheel.length - 1;
对应结构如下:

3.2 tickDuration

        // Convert tickDuration to nanos.
        // 转化为纳秒
        long duration = unit.toNanos(tickDuration);

        // Prevent overflow.
        if (duration >= Long.MAX_VALUE / wheel.length) {
            throw new IllegalArgumentException(String.format(
                    "tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
                    tickDuration, Long.MAX_VALUE / wheel.length));
        }

        if (duration < MILLISECOND_NANOS) {
            logger.warn("Configured tickDuration {} smaller than {}, using 1ms.",
                        tickDuration, MILLISECOND_NANOS);
            this.tickDuration = MILLISECOND_NANOS;
        } else {
            this.tickDuration = duration;
        }

根据long tickDuration, TimeUnit unit 得出每次hash时钟摆动的间隔值

3.3 workerThread

workerThread = threadFactory.newThread(worker); 根据线程工厂,构建出工作线程,也就是通过该线程进行了延迟任务的执行

3.4 other

        leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;
// 最大允许等待任务数,HashedWheelTimer 中任务超出该阈值时会抛出异常
        this.maxPendingTimeouts = maxPendingTimeouts;

// 如果实例数超过 64,会打印错误日志
        if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
            WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
            reportTooManyInstances();
        }



private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(); 类常量



最多创建64个实例?
HashedWheelTimer is a shared resource that must be reused across the JVM
so that only a few instances are created
根据报错提示,可以知道这是一个共享资源,可以进行reused,所以只需要创建少量就行

3.5 疑问

通过初始化函数得知,只是创建了工作线程,并未进行工作线程的执行,那么工作线程的start在什么时候调用?以及内存泄漏检测原理?

4 添加任务

    @Override
    public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        checkNotNull(task, "task");
        checkNotNull(unit, "unit");

        long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();

        if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
            pendingTimeouts.decrementAndGet();
            throw new RejectedExecutionException("Number of pending timeouts ("
                + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
                + "timeouts (" + maxPendingTimeouts + ")");
        }
        // 工作线程要是没启用,则进行启用,等启动线程执行唤醒后,才会新增对应的定时任务
        start();

        // Add the timeout to the timeout queue which will be processed on the next tick.
        // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
        // 截至时间,是一个差值,相对时间
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

        // Guard against overflow.
        if (delay > 0 && deadline < 0) {
            deadline = Long.MAX_VALUE;
        }
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);//创建定时任务
        timeouts.add(timeout); // 加入队列
        return timeout;
    }
private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();// Mpsc Queue 可以理解为多生产者单消费者的线程安全队列后续介绍

小结:newTimeout() 方法主要做了三件事,分别为启动工作线程,创建定时任务,并把任务添加到 Mpsc Queue。HashedWheelTimer 的工作线程采用了懒启动的方式,不需要用户显示调用。这样做的好处是在时间轮中没有任务时,可以避免工作线程空转而造成性能损耗。

// Initialize the startTime.
startTime = System.nanoTime(); startTime 是在任务开始执行后,才进行赋值的所以要添加阻塞startTimeInitialized 进行阻塞。

4.1 start()

    public void start() {
        switch (WORKER_STATE_UPDATER.get(this)) {
            case WORKER_STATE_INIT:
                if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
                    workerThread.start();
                }
                break;
            case WORKER_STATE_STARTED:
                break;
            case WORKER_STATE_SHUTDOWN:
                throw new IllegalStateException("cannot be started once stopped");
            default:
                throw new Error("Invalid WorkerState");
        }

        // Wait until the startTime is initialized by the worker.
        while (startTime == 0) {
            try {
                startTimeInitialized.await();
            } catch (InterruptedException ignore) {
                // Ignore - it will be ready very soon.
            }
        }
    }

4.2 疑问

什么时候进行时间轮的执行?workThread执行

5 工作线程的执行

        public void run() {
            // Initialize the startTime.
            startTime = System.nanoTime();
            if (startTime == 0) {
                // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
                startTime = 1;
            }

            // Notify the other threads waiting for the initialization at start().
            startTimeInitialized.countDown();

            do {
                final long deadline = waitForNextTick(); // 计算下次 tick 的时间, 然后sleep 到下次 tick
                if (deadline > 0) { // 可能因为溢出或者线程中断,造成 deadline <= 0
                    int idx = (int) (tick & mask); // 获取当前 tick 在 HashedWheelBucket 数组中对应的下标
                    processCancelledTasks(); // 移除被取消的任务
                    HashedWheelBucket bucket =
                            wheel[idx];
                    transferTimeoutsToBuckets();//从 Mpsc Queue 中取出任务加入对应的 slot 中
                    bucket.expireTimeouts(deadline);//  执行到期的任务
                    tick++;
                }
            } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

            // Fill the unprocessedTimeouts so we can return them from stop() method.
            // 时间轮退出后,取出 slot 中未执行且未被取消的任务,并加入未处理任务列表,以便 stop() 方法返回
            for (HashedWheelBucket bucket: wheel) {
                bucket.clearTimeouts(unprocessedTimeouts);
            }
           // 将还没添加到 slot 中的任务取出,如果任务未取消则加入未处理任务列表,以便 stop() 方法返回
            for (;;) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    break;
                }
                if (!timeout.isCancelled()) {
                    unprocessedTimeouts.add(timeout);
                }
            }
            processCancelledTasks();
        }

前面提到start()方法中会启动worker线程,并且会等待startTime不为0,worker线程会把startTime设置为当前的纳秒时间,并且startTimeInitialized.countDown()唤醒阻塞在start()方法的线程。

5.1 waitForNextTick

        private long waitForNextTick() {
            long deadline = tickDuration * (tick + 1); // (根据当前tick+1) * tickDuration 每次转动的间隔的时间得出截至时间,如当前ticket为1,间隔为1s,则deadline 为2*1 = 2s,表示2s后执行

            for (;;) {
                final long currentTime = System.nanoTime() - startTime; // 当前时间-开始时间表示当前已经过了currentTime 的时间差值,deadline - currentTime则可以得出还剩多少时间可以进行等待,因为转为了纳秒为单位,所以只要还有1纳秒,就需要进行继续sleep,所以进行+ 999999的流程
                long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;
                // 若小于0,则表示需要立刻执行,返回currentTime差值时间
                if (sleepTimeMs <= 0) {
                    if (currentTime == Long.MIN_VALUE) {
                        return -Long.MAX_VALUE;
                    } else {
                        return currentTime;
                    }
                }

                // Check if we run on windows, as if thats the case we will need
                // to round the sleepTime as workaround for a bug that only affect
                // the JVM if it runs on windows.
                //
                // See https://github.com/netty/netty/issues/356
                if (PlatformDependent.isWindows()) {
                    sleepTimeMs = sleepTimeMs / 10 * 10;
                    if (sleepTimeMs == 0) {
                        sleepTimeMs = 1;
                    }
                }

                try {
                    Thread.sleep(sleepTimeMs);
                } catch (InterruptedException ignored) {
                    if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                        return Long.MIN_VALUE;
                    }
                }
            }
        }

5.1 transferTimeoutsToBuckets

  private void transferTimeoutsToBuckets() {
            // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
            // adds new timeouts in a loop.
            for (int i = 0; i < 100000; i++) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    // all processed
                    break;
                }
                if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
                    // Was cancelled in the meantime.
                    continue;
                }

                long calculated = timeout.deadline / tickDuration;
                timeout.remainingRounds = (calculated - tick) / wheel.length;

                final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
                int stopIndex = (int) (ticks & mask);

                HashedWheelBucket bucket = wheel[stopIndex];
                bucket.addTimeout(timeout);
            }
        }

transferTimeoutsToBuckets() 的主要工作就是从 Mpsc Queue 中取出任务,然后添加到时间轮对应的 HashedWheelBucket 中。每次时针 tick 最多只处理 100000 个任务,一方面避免取任务的操作耗时过长,另一方面为了防止执行太多任务造成 Worker 线程阻塞。

根据用户设置的任务 deadline,可以计算出任务需要经过多少次 tick 才能开始执行 以及需要在时间轮中转动圈数 remainingRounds,remainingRounds 会记录在 HashedWheelTimeout 中,在执行任务的时候 remainingRounds进行判断当前是否执行。因为时间轮中的任务并不能够保证及时执行,假如有一个任务执行的时间特别长,那么任务在 timeouts 队列里已经过了执行时间,也没有关系,Worker 会将这些任务直接加入当前HashedWheelBucket 中,所以过期的任务并不会被遗漏。

5.2  HashedWheelBucket#expireTimeouts() 

        public void expireTimeouts(long deadline) {
            HashedWheelTimeout timeout = head;

            // process all timeouts
            while (timeout != null) {
                HashedWheelTimeout next = timeout.next;
                if (timeout.remainingRounds <= 0) {
                    next = remove(timeout);
                    if (timeout.deadline <= deadline) {
                        timeout.expire(); // 执行任务
                    } else {
                        // The timeout was placed into a wrong slot. This should never happen.
                        throw new IllegalStateException(String.format(
                                "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
                    }
                } else if (timeout.isCancelled()) {
                    next = remove(timeout);
                } else {
                    timeout.remainingRounds --; // 未到执行时间,remainingRounds 减 1
                }
                timeout = next;
            }
        }

执行任务即: taskExecutor进行

execute方法
        public void expire() {
            if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
                return;
            }

            try {
                timer.taskExecutor.execute(this);
            } catch (Throwable t) {
                if (logger.isWarnEnabled()) {
                    logger.warn("An exception was thrown while submit " + TimerTask.class.getSimpleName()
                            + " for execution.", t);
                }
            }
        }
  • HashedWheelTimeout,任务的封装类,包含任务的到期时间 deadline、需要经历的圈数 remainingRounds 等属性。
  • HashedWheelBucket,相当于时间轮的每个 slot,内部采用双向链表保存了当前需要执行的 HashedWheelTimeout 列表。
  • Worker,HashedWheelTimer 的核心工作引擎,负责处理定时任务。

6 缺点

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

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

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

相关文章

用纯html写个个人简历!模版分享啦!!!

用纯html写个个人简历&#xff01;首先得先找个模板&#xff01; 一个优秀模板所应该具有的素质&#xff1f; 简单&#xff1f; 仅仅一个html页面&#xff0c;完全没有乱七八糟&#xff0c;保证学的明明白白。 漂亮&#xff1f; 该有的内容一个不少&#xff01; 个人照片&a…

零基础Linux_6(开发工具_下)函数库链接+Makefile+实现进度条+Git

目录 1. 函数库&#xff08;链接&#xff09; 1.1 链接 1.2 动态库与静态库 2. makefile 2.1 项目构建 2.2 Makefile的概念 2.3 Makefile的编写 2.4 .PHONY定义伪目标 ACM 时间 3.实现进度条&#xff08;缓冲区&#xff09; 3.1 缓冲区的概念 3.2 实现一个简易 &q…

全网最详细的OSPF原理总结,看这篇就够了!

大家好&#xff0c;我的网工朋友。 OSPF是一种基于链路状态的路由协议&#xff0c;也是专为 IP 开发的路由协议&#xff0c;直接运行在 IP 层上面。它从设计上保证了无路由环路。除此之外&#xff0c;IS-IS也是很常见的链路状态协议。 为什么会出现OSPF&#xff1f; 作为目前…

欧拉操作系统在线安装mysql8数据库并用navicat premium远程连接

网上太多安装教程&#xff0c;但是没有一个教程能够一站式解决安装问题&#xff0c;而我不一样&#xff0c;我写的每一个博客&#xff0c;都是我自己试验过很多次&#xff0c;能够确保一站式解决&#xff0c;才会发上去&#xff0c;希望能够帮助到大家 第一步&#xff1a;升级…

Linux工具(一)

前言&#xff1a;Linux是一个开源的操作系统&#xff0c;它拥有庞大而活跃的开发社区&#xff0c;为用户提供了丰富多样的工具和应用程序。这些工具不仅适用于系统管理员和开发人员&#xff0c;也适用于普通用户&#xff0c;可以帮助他们完成各种任务&#xff0c;从简单的文件管…

爬虫 — Scrapy 框架安装问题

整理几个关于安装 Scrapy 框架时会遇到的问题及解决方法。 1、 pip install typing-extensions4.3.0 -i https://pypi.douban.com/simple 2、 pip install pyOpenSSL22.0 -i https://pypi.douban.com/simple 3、 pip install cryptography36.0.2 -i https://pypi.douban.com/s…

基于GBDT+Tkinter+穷举法按排队时间预测最优路径的智能导航推荐系统——机器学习算法应用(含Python工程源码)+数据集(四)

目录 前言总体设计系统整体结构图系统流程图 运行环境Python环境Pycharm 环境Scikit-learnt 模块实现1. 数据预处理2. 客流预测3. 百度地图API调用4. GUI界面设计5. 路径规划6. 智能推荐 系统测试1. 训练准确率2. 测试效果3. 程序应用 相关其它博客工程源代码下载其它资料下载 …

如何在云服务器上成功安装MongoDB数据库并用Python连接(问题及解决方法)

最近购买了1台腾讯云轻量服务器做测试&#xff0c;想在上面安装MongoDB数据库。但安装过程并不顺利&#xff0c;遇到了几个问题。本文记录一下安装过程及遇到的问题和解决方法。 一、软件下载地址&#xff1a; 1.直接打开MongoDB官网&#xff1a;https://www.mongodb.com/ …

2023 Google 开发者大会:助力传承和弘扬传统文化

文章目录 前言一、关于 Google 开发者大会1.1、什么是 Google 开发者大会&#xff1f;1.2、CSDN 上线 2023 Google 开发者大会专题页 二、敦煌深厚的艺术沉淀2.1、“云想衣裳花想容”&#xff1f;2.2、“大漠孤烟直&#xff0c;长河落日圆” 三、传统文化赋予现代艺术设计灵感四…

1.IAR-8051安装

新版安装教程&#xff1a;IAR EW for 8051 简介与安装 新版软件zhuce&#xff1a;IAR EW for 8051 软件注册 - 知乎 这个新版的我也放到网盘里面了&#xff0c;自己自行选择安装 一、下载IAR-8051 链接&#xff1a;https://pan.baidu.com/s/1mYwSQvSjAiSzltrEbK3yAw?pwd43cd …

IPV4和IPV6,公网IP和私有IP有什么区别?

文章目录 1、什么是IP地址&#xff1f;1.1、背景1.2、交换机1.3、局域网1.4、广域网1.5、ISP 互联网服务提供商 2、IPV42.1、什么是IPV4&#xff1f;2.2、IPV4的组成2.3、NAT 网络地址转换2.4、端口映射 3、公网IP和私有IP4、IPV6 1、什么是IP地址&#xff1f; 1.1、背景 一台…

接口调用三种方式

创建服务端 一个controller&#xff0c;一个启动类&#xff0c;配置端口 controller import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;RestController public class ServerController {Req…

uniapp开发h5 遇到一个刻度尺的需求 实现记录

先上最终实现图 实现这个功能使用的是github上找的一个库 叫slide-ruler 地址 实现思路&#xff1a; 第一步&#xff1a;在终端npm 下载这个依赖 第二步&#xff1a;新建一个刻度尺自定义组件&#xff0c;如果不是自定义组件会不生效&#xff0c;并且这个自定义组件里面放一…

用无代码搭建数据中台,竟做到如此丝滑

文章目录 需求背景系统介绍配置说明1 菜单导航2 系统自带组件导入页面&#xff08;1&#xff09;数据集成相关组件&#xff08;2&#xff09;数据服务相关组件 3 由系统组件路径添加页面&#xff08;1&#xff09;数据资产管理&#xff08;2&#xff09;数据标准管理&#xff0…

Hutool工具包:http客户端工具(使用教程)

目录 一、Hutool介绍 二、笔者的话 三、引入依赖 四、大致步骤 五、GET请求 5.1 代码 5.2 结果展示 六、POST请求 6.1 代码一&#xff08;Form Data类型参数&#xff09; 6.2 结果展示 6.3 代码二&#xff08;Form Data类型参数 - 含上传文件&#xff09; 6.4 结果…

idea设置项目启动的JVM运行内存大小

idea设置项目启动的JVM运行内存大小 场景 在开发当中&#xff0c;idea默认服务启动要占用1G内存。其实每个项目本地开发和调试的时候&#xff0c;根本不需要1G内存&#xff0c;200M左右足以 如果在微服务体系下&#xff0c;那效果更明显&#xff0c;相同的内存可以启动更多的…

Linux学习之Redis集群部署

Redis集群部署 准备集群环境 创建集群 # 准备集群环境--配置192.168.88.51(host51) [rootlocalhost ~]# yum install -y redis [roothost51 ~]# vim /etc/redis.conf bind 192.168.88.51 cluster-enabled yes cluster-config-file nodes-6379.conf cluster-node-timeout 5000…

Java web基础知识

Servlet Servlet是sun公司开发的动态web技术 sun在API中提供了一个接口叫做 Servlet &#xff0c;一个简单的Servlet 程序只需要完成两个步骤 编写一个实现了Servlet接口的类 把这个Java部署到web服务器中 一般来说把实现了Servlet接口的java程序叫做&#xff0c;Servlet 初步…

成功的海外网红营销:文化和价值观冲突的应对策略

随着全球数字化和社交媒体的崛起&#xff0c;海外网红营销已经成为企业推广产品和服务的一种重要方式。然而&#xff0c;这种全球性的营销活动也伴随着文化和价值观的多样性&#xff0c;容易导致潜在的冲突和误解。为了取得成功并避免不必要的争议&#xff0c;企业需要深入了解…

从B-21轰炸机看美空军作战战略趋势

源自&#xff1a;北京蓝德信息科技有限公司 图注&#xff1a;The B-21 Raider was unveiled to the public at a ceremony Dec. 2, 2022, in Palmdale, Calif. (U.S. Air Force photo) (一)B-21开发进展 (二)B-21性能研判 声明:公众号转载的文章及图片出于非商业性的教育和科研…