现在做任务调度的组件有很多,在使用的过程中有一个疑问,那就是任务真的可以被按时执行吗。如果不能主要的影响是什么。
下面以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个线程。此时也只能等待现有的线程执行完之后,才可以执行,这里的时间肯定是有一定的误差。