ScheduledExecutor和DelayQueue,Java任务延迟执行的秘密

Java延迟执行:ScheduledExecutor与DelayQueue解析
本文探讨了Java中ScheduledExecutorService如何实现任务延迟执行,对比了Timer的缺陷,重点分析了ScheduledThreadPoolExecutor的DelayQueue工作原理,包括其如何确保线程不被阻塞、延迟时间的准确性以及周期任务的执行方式。通过源码解析,阐述了ScheduledFutureTask在延迟队列中的角色和任务调度的细节。

引言

Timer类的解析中提到,Timer类的缺陷:

  1. 单线程
  2. Timer只有一个任务执行线程,如果某个TimerTask耗时较久,影响其他任务
  3. Timer不会捕获执行TimerTask时所抛出的异常,由于Timer是单线程,所以一旦出现异常,则线程就会终止,其他任务也得不到执行。
  4. 系统时间敏感性,由于Timer中使用了currentTimeMillis做参考,所以系统时间变化会使得任务发生变化

所以在JDK1.5后大家就抛弃了Timer,一般使用ScheduledThreadPoolExecutor。

研究完ScheduledExecutor,我突然发现一直困扰我的一个问题——程序中的定时任务是如何实现的?例如,闹钟。ScheduledExecutor正是一个绝佳的例子:
● 任务调度和任务执行解耦,并运行在不同线程上,防止阻塞。任务调度单线程即可满足性能要求,它并不是瓶颈
● 具备延迟时间的任务,在时间的维度上,可以假想它们排成一列,所以你不需要轮询所有任务,只需要关注最早开始的任务
● 等待并不需要轮询,可以等待指定时间唤醒

概述

我们已经看过Timer的延迟执行方案——任务放入最小堆中,任务线程每次取出需要最早执行的任务,并在当前线程中执行。
ScheduledThreadPoolExecutor简单来说,是基于线程池的实现基础上,把原有的任务队列换成DelayQueue。
所以我认为,核心其实在DelayQueue上,它是一种阻塞队列,只有当任务到指定延迟时间时,它才能返回;其次,它也是一种优先队列,基于数组的最小堆实现,按延迟时间排序。
由于取出任务之后,交给线程池执行(由线程池管理子线程执行),所以并不会阻塞调度线程,所以也就没有上述1,2,3的问题。而且它内部使用了nanoTime而不是currentTimeMillis,因而也和系统时间无关(nanoTime的区别是,它是计算从某一个时间的累计时间,这个时间是在启动JVM时任意指定的,例外,它的精度为微纳秒)。
所以它本身不复杂,线程池的实现参考之前的文章。

源码解析

API

ScheduledFuture<?> scheduleAtFixedRate(Runnable command,**long **initialDelay,**long **period,TimeUnit unit),指定任务,初始延迟时间,周期时间,时间单位。注意该方法每一周期启动一次任务,不需要等任务结束。
scheduleWithFixedDelay(Runnable command,**long **initialDelay,**long **delay,TimeUnit unit),指定任务,初始延迟时间,延迟时间,时间单位。注意该延迟时间指的是前一个周期结束到下一个周期开始时间,换句话说,它需要任务执行完才能开始下一个周期。

代码结构

请添加图片描述

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,复用线程池的功能。实现了ScheduledExecutorService,拓展为调度的线程池服务。

public interface ScheduledExecutorService extends ExecutorService {

    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
    
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
}

DelayedWorkQueue **extends **AbstractQueue<Runnable>**implements **BlockingQueue<Runnable>,队列则继承了AbstractQueue,数组实现了最小堆。实现了BlockingQueue接口,具备阻塞队列的特性。
最后就是队列中放的对象,为ScheduledFutureTask,它的继承结构比较复杂,但本质上就是对FutureTask的拓展,支持了延迟时间的对比查询管理。
请添加图片描述

源码

ScheduledThreadPoolExecutor的入口,都很简单,核心是队列的阻塞方法——如何实现到时间才返回。

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0L)
        throw new IllegalArgumentException();
    // 新建了一个延迟任务
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period),
                                      sequencer.getAndIncrement());
    // decorateTask其实是一个就是隐式的向上转型,转为RunnableScheduledFuture
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    // 调度该任务
    delayedExecute(t);
    return t;
}
private void delayedExecute(RunnableScheduledFuture<?> task) {
    if (isShutdown())
        reject(task);
    else {
        // 任务加入队列
        super.getQueue().add(task);
        if (!canRunInCurrentRunState(task) && remove(task))
            task.cancel(false);
        else
            // 线程池的方法,确认是否添加工作线程
            ensurePrestart();
    }
}

加入任务队列后,其实剩下都是线程池的工作,它的工作线程会不断地从任务队列中取出任务,区别在于我们的任务队列是DelayedQueue。
所以核心就是理解它如何阻塞线程,让任务到时间再返回,来看take方法。
该Queue持有一个leader,它是当前执行的线程引用。

public RunnableScheduledFuture<?> take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 调度过程加锁,注意lock用于锁定调度
    lock.lockInterruptibly();
    try {
        for (;;) {
            RunnableScheduledFuture<?> first = queue[0];
            // 堆为空,等待,available为锁上的condition监视器
            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
                // 如果leader不为空,则说明已经有线程占用,available上等待
                if (leader != null)
                    // 除了leader的其他线程无限等待
                    available.await();
                // leader为空,则自己作为leader线程
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // leader等待指定时间
                        available.awaitNanos(delay);
                    } finally {
                        // 执行完,设置leader为null
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 当前任务调度完成,唤醒其他线程调度下一任务
        if (leader == null && queue[0] != null)
            available.signal();
        lock.unlock();
    }
}

我们看到,它的逻辑也不复杂,如果任务达到执行时间,就立刻返回给线程池去执行。每次只有一个线程能做leader,拥有调度当前任务的权限,等待指定延迟时间,其他线程则无限等待,直到被唤醒。
再来说一下leader的作用,这里的leader是为了减少不必要的定时等待,当一个线程成为leader时,它只等待下一个节点的时间间隔,但其它线程无限期等待。 leader线程必须在从take()或poll()返回之前signal其它线程,除非其他线程成为了leader。

然后就是返回任务的时候,queue又做了什么?
其实很简单,将堆顶元素返回,堆顶元素移除,替换为最后一个元素,然后调整堆。

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;
}

最后是周期执行如何实现?
答案是,在任务执行完,重新设置下一次的延迟时间,重新入queue调度。

public void run() {
    if (!canRunInCurrentRunState(this))
        cancel(false);
    // 非周期执行,直接执行
    else if (!isPeriodic())
        super.run();
    // 周期执行,执行完重新设置时间
    else if (super.runAndReset()) {
        // 重新设置时间
        setNextRunTime();
        // 重新入queue
        reExecutePeriodic(outerTask);
    }
}

为何不精确

仔细考虑一下整个实现,你会发现:

  1. 任务执行交给线程池管理
  2. 调度任务每次只有一个线程能进入,都会等queue中需要最早执行的任务到达执行时间,交给线程池执行。因为一个任务调度很快,所以多个任务先后到达执行时间都会执行,而不像Timer一样,受到前一个任务执行影响
  3. 由于堆的调整需要时间,线程池的线程资源有限,所以任务也不一定非常精确地按照延迟时间来。

参考

http://concurrent.redspider.group/article/03/20.html

"Thread-157" J9VMThread:0x0000000004360600, omrthread_t:0x00007FA034013DA0, java/lang/Thread:0x00000007088F08F0, state:CW, rawStateValue:0x8, prio=5 3XMJAVALTHREAD (java/lang/Thread getId:0xD8E8, isDaemon:false) 3XMJAVALTHRCCL sun/misc/Launcher$AppClassLoader(0x00000007001CF9C0) 3XMTHREADINFO1 (native thread ID:0x1FD6D, native priority:0x5, native policy:UNKNOWN, vmstate:CW, vm thread flags:0x00000481) 3XMTHREADINFO2 (native stack address range from:0x00007F9FF4EFC000, to:0x00007F9FF4F3C000, size:0x40000) 3XMCPUTIME CPU usage total: 32.466785700 secs, current category="Application" 3XMHEAPALLOC Heap bytes allocated since last GC cycle=28672 (0x7000) 3XMTHREADINFO3 Java callstack: 4XESTACKTRACE at java/lang/Thread.sleepImpl(Native Method) 4XESTACKTRACE at java/lang/Thread.sleep(Thread.java:974(Compiled Code)) 4XESTACKTRACE at java/lang/Thread.sleep(Thread.java:957(Compiled Code)) 4XESTACKTRACE at com/jsfj/ects/infrastructure/immsg/RedisDelayQueue.listenDelayLoop(RedisDelayQueue.java:71(Compiled Code)) 4XESTACKTRACE at com/jsfj/ects/infrastructure/immsg/CustomerThread.run(CustomerThread.java:31(Compiled Code)) 3XMTHREADINFO3 Native callstack: 4XENATIVESTACK (0x00007FA133074A02 [libj9prt29.so+0x25a02]) 4XENATIVESTACK (0x00007FA133079049 [libj9prt29.so+0x2a049]) 4XENATIVESTACK (0x00007FA133074EDE [libj9prt29.so+0x25ede]) 4XENATIVESTACK (0x00007FA133079049 [libj9prt29.so+0x2a049]) 4XENATIVESTACK (0x00007FA133074887 [libj9prt29.so+0x25887]) 4XENATIVESTACK (0x00007FA133075DDC [libj9prt29.so+0x26ddc]) 4XENATIVESTACK (0x00007FA132D68630 [libpthread.so.0+0xf630])
最新发布
11-12
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值