ScheduledThreadPool的设计思路和扩展

440 阅读4分钟

现在做任务调度的组件有很多,在使用的过程中有一个疑问,那就是任务真的可以被按时执行吗。如果不能主要的影响是什么。

下面以java ScheduledThreadPool为案例进行解读。

ScheduledThreadPool

ScheduledThreadPool是基于ThreadPoolExecutor实现的,所以他的调度的方法都是ThreadPoolExecutor的。简单概况就是:核心线程不足,增加核心线程数,启动更多的线程,达到之后,任务会放在阻塞队列中。阻塞队列满了之后,就会继续增加线程处理,直到线程池的最大线程数。

ScheduledThreadPool的核心设计就在阻塞队列了。我们看他的构造方法。


public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

DelayedWorkQueue是一个内部类,是一个小顶堆。按照task的执行时间排序,时间越小的越靠前。这里其实我们就理解了所谓的调度过程,就是在做时间的比大小。时间满足执行了,就可以执行了。

我们结合上面的调度策略来看,线程池的线程要从阻塞队列里获取任务。任务如果没有获取到,进入阻塞的状态。这里一定有一个问题,就是假如我只有1个任务在定时调度。他怎么自己唤醒自己呢?

带着这个问题,我们从任务和阻塞队列2个角度一起来看一下。

一个任务的循环

long delay = first.getDelay(NANOSECONDS); 
if (delay <= 0)
return finishPoll(first);

阻塞队列的take方法会对比时间,满足了时间要求,就会返回任务,交给线程池执行,在线程池执行的任务执行完之后。

public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

又会把任务的时间重置,然后再次提交到队列里。此时的队列就会触发一个新增任务。

多线程竞争设计

在阻塞队列的实现里有一个特殊的角色。leader。

if (queue[0] == e) {
    leader = null;
    available.signal();
}

offer的方法中如果队列里的第一个任务就是当前任务就会把 leader = null;然后唤醒其他阻塞的线程。 从offer的过程可以看出,在两种情况会队列第一个是e,第一个是当前没有任务,第二个是当前的任务经过排序,成为了最近执行的任务。这里的signal可以唤醒任意一个等待的线程。

take的过程状态就比较多了。 第一种状态没有任务,就会直接阻塞线程等待被唤醒。

RunnableScheduledFuture<?> first = queue[0];
if (first == null)
    available.await();

第二种状态是拿到了任务发现可以执行,直接返回。 第三种状态是线程成了唯一1个看到任务的等待时间,然后等待对应的时间,后续在执行。在等待的过程会把自己设置为leader,在等待结束后去释放leader的状态。

Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
    available.awaitNanos(delay);
} finally {
    if (leader == thisThread)
        leader = null;
}

第四种状态是虽然被唤醒了,但是发现其他线程已经是leader了,已经有一个线程在等任务了,此时就进入等待状态。

if (leader != null)
    available.await();

这里我们模拟多线程的场景。先提了一个等待3s执行的任务。此时线程1就会等待3s再获取,然后又提了等待2s执行的任务,此时会把leader去掉,并且唤醒新的线程,线程此时拿到新的任务,发现不能执行,然后又开始等待,由于此时没有leader,新的线程就会成为leader等待。

leader的作用

通过描述,我们发现leader的主要作用就是作为第一个获取头任务的线程,他可以等待一定时间然后自己唤醒自己,继续做检查。这里的好处就是减少了多线程的无异议竞争,假如有1个任务,其实有1个线程等待就可以了,不用所有线程都在定时等待。多线程的情况下,会出现多个线程分别等待不同的头任务,造成的原因就是新的提交。

解答

再看开始的问题,我们其实比较明确了,执行肯定是不一定的,算力是有限的,例如3个时间满足的任务,但是只有2个线程。此时也只能等待现有的线程执行完之后,才可以执行,这里的时间肯定是有一定的误差。