源码分析与实战——深入理解ScheduledThreadPool线程池延时执行机制(一)

1、前言

在上篇博客《源码分析与实战——深入理解Java的4种线程池》中,我们详细分析了一下Java四种线程池的基本源码,编写代码进行了尝试。其中single单线程池、fiexed定长线程池、cached缓存线程池都比较简单,scheduled线程池则复杂一些。今天我们结合延迟队列来对它进行源码分析,详细讲解一下延时执行线程池的工作原理。

2、线程池定义

首先,我们还是再来看一下最简单的一个使用示例:

public class TestSchedule {
    public static void main(String[] args) {
        //此时不可再用ExecutorService
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        System.out.println("scheduledExecutorService start ...");

        //延时任务
        scheduledExecutorService.schedule(() -> System.out.println("exec a task ..."), 1, TimeUnit.SECONDS);
        System.out.println("scheduled a task ...");
    }
}

执行结果:

scheduledExecutorService start ...
scheduled a task ...
exec a task ...

我们可以注意到,这里我们使用的是ScheduledExecutorService,其他3个线程池都是直接用ExecutorService即可。

然后再次看看Executors.newScheduledThreadPool()方法:

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

为了实现延时执行线程池,Java专门定义了一个新类ScheduledThreadPoolExecutor去实现它。
类定义如下:

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
        ......
}

它有一个父类ThreadPoolExecutor,实现了接口ScheduledExecutorService。

2.1 ThreadPoolExecutor类

ScheduledThreadPoolExecutor类继承了ThreadPoolExecutor类,它是实现线程池的主类,其他3种线程池都是直接利用这个类来实现的。我们甚至可以利用它来扩展自定义的新的线程池,这个以后有机会再单独讲。

我们看看new ScheduledThreadPoolExecutor(corePoolSize);
这里的内容在上篇博文中其实我已经详细讲过了,但是本篇我觉得应该也可以独立阅读,所以重复一下,看过那篇的朋友可以跳过。

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

ScheduledThreadPoolExecutor调用了父类的构造方法,也就是ThreadPoolExecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

最终实际调用的是:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
  		......
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

通过这个调用链,我们得到的结果是:

  1. 通过corePoolSize指定了线程池里线程的数量;
  2. 从keepAliveTime为0可以看出,线程空闲时并不释放,会一直存在,等待新任务;
  3. 线程池所使用的队列是DelayedWorkQueue,它是一个阻塞队列。DelayedWorkQueue是ScheduledThreadPoolExecutor中定义的一个内部类,它非常关键,后面单独讲。

这个构造过程,很明显只是一个初始化过程!

2.2 ScheduledExecutorService接口

ScheduledThreadPoolExecutor类实现了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);   
}                                                                                                                                                                             

很明显,我们操作这个线程池的时候,使用的就是这4个接口方法,因此我们获取newScheduledThreadPool()的结果的时候,用的是ScheduledExecutorService:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);

ScheduledThreadPoolExecutor类重写了这4个接口方法,结合ThreadPoolExecutor类中的方法,实现了延时执行的功能。

3、schedule接口方法实现

schedule方法,是延时执行线程池使用最多的方法,源代码如下:

    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                                          triggerTime(delay, unit)));
        delayedExecute(t);
        return t;
    }

这里面就2个核心点:

  1. 创建ScheduledFutureTask的对象,其实就是新建一个延时任务;
  2. 调用延时方法。

3.1 ScheduledFutureTask初始化

decorateTask只是个包装类,可以不用管它,关键是这个新建了一个 ScheduledFutureTask的对象。这个类是ScheduledThreadPoolExecutor的一个内部类,它的作用是创建一个延时执行的任务。这个任务会在什么时候执行,由triggerTime(delay, unit)指定:

    private long triggerTime(long delay, TimeUnit unit) {
        return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
    }
	......
    long triggerTime(long delay) {
        return now() +
            ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
    }    

从上面源码可以看出,它返回的是一个时间,从now()当前时间开始往后推delay,即我们加入任务时,所指定的延时时间。总而言之,triggerTime()返回了这个任务执行的时间时间,这是一个绝对时间。

我们再看这里所调用的ScheduledFutureTask构造方法:

        ScheduledFutureTask(Runnable r, V result, long ns) {
            super(r, result);
            this.time = ns;
            this.period = 0;
            this.sequenceNumber = sequencer.getAndIncrement();
        }

很简单,这里只有几个赋值,只是最开始的初始化。
它只是把这个新任务执行的绝对时间赋值给了类的成员time,这个延时任务是一次性执行完成的,所以period为0。

3.2 delayedExecute()方法

第二步就是调用了:delayedExecute(t);

    private void delayedExecute(RunnableScheduledFuture<?> task) {
        if (isShutdown())
            reject(task);
        else {
            super.getQueue().add(task);
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                ensurePrestart();
        }
    }

通过源码,可以清晰的看到:任何情况下添加任务,第一步就是执行:

super.getQueue().add(task);

无论队列空的、还是工作线程空闲,系统其实什么都不管,先把新任务加入队列再说!因此,很多相关的文档是这样描述的:“如果当前线程池中没有空闲的线程,就将新任务放入等待队列"、"第一个任务加入时,会创建一个新线程,去执行这个任务”,等等,这些都是不正确的说法。实际是无论什么情况,新任务都是先加入队列再说!只不过,当前线程池里面有工作线程空闲或者线程数没有达到指定数量时,这个新任务会很快从队列里面取出并执行。后面从源代码可以很简单的看出来!

3.2.1 DelayedWorkQueue的add()

这里面getQueue(),取得是初始化时候创建的DelayedWorkQueue队列,我们看一下这个队列的add()方法:

        public boolean add(Runnable e) {
            return offer(e);
        }
 public boolean offer(Runnable x) {
            if (x == null)
                throw new NullPointerException();
            RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                int i = size;
                if (i >= queue.length)
                    grow();
                size = i + 1;
                if (i == 0) {
                    queue[0] = e;
                    setIndex(e, 0);
                } else {
                    siftUp(i, e);
                }
                if (queue[0] == e) {
                    leader = null;
                    available.signal();
                }
            } finally {
                lock.unlock();
            }
            return true;
        }

逻辑很简单:

  1. 如果i大于当前queue的长度时,使用grow()对队列扩容
  2. i==0时意味着是新队列,直接放到队列头;
  3. i不等于0时,通过siftUp()将新任务放到合适的位置
  4. queue[0] == e,如果当前任务就放在队列头了,直接发信号唤醒阻塞中的线程,开始执行队列头的任务
queue说明

queue是什么呢?我们也可以看一下:

		private static final int INITIAL_CAPACITY = 16;
        private RunnableScheduledFuture<?>[] queue =
            new RunnableScheduledFuture<?>[INITIAL_CAPACITY];

queue是一个数组,初始大小为16。也就说,线程池的阻塞队列默认长度是16,可以放入16个任务。

grow()方法

然后再看看grow()方法:

  		private void grow() {
            int oldCapacity = queue.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
            if (newCapacity < 0) // overflow
                newCapacity = Integer.MAX_VALUE;
            queue = Arrays.copyOf(queue, newCapacity);
        }

当队列容量不够时,直接将队列长度扩大50%!也就是说,这也是一个无界队列,使用不当可能会导致队列里任务过多而OOM!

siftUp()方法

再看看siftUp()方法:

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

(k-1)>>>1,即将k减1后无符号右移1位,也就是减1后除以2。假设当前k的值为33,那么parent就是16。拿当前任务和数组下标为16的任务进行比较,比的是2个任务对象里面time的大小,time越大,执行时间越晚,因此逻辑是:

  1. 如果当前任务更早执行,返回-1;
  2. 如果当前任务更晚执行,返回1;
  3. 如果2个任务的time一样,比sequenceNumber:如果当前任务sequenceNumber小,返回-1;否则返回1。

key.compareTo(e) >= 0意味着当前任务比parent对应的任务更晚,所以位置不动,直接break出来;否则,通过while循环不停的向上查找,直到找到一个合适的位置为止。在这个过程中,每一次比较都会交换2个任务的位置。通过这种方式,将新任务插入到合适的位置。大家可以看到,这个不是一个逐个比较的过程,其实是二分查找,目的是为了提高效率。

提个问题:假设queue[33]的time是100,queue[32]的time是200,queue[16]的time是50,那么会发生什么事情?
此时,我们发现key.compareTo(e) >= 0成立,break执行了,没有进行任何交换直接跳过!如果这个队列顺序要求很精确,那么queue[33]应该比queue[32]先执行吗?这个问题内容稍多,我放到下一篇单独讲。

3.2.2 ensurePrestart()方法

接下来就是执行ensurePrestart();

    void ensurePrestart() {
        int wc = workerCountOf(ctl.get());
        if (wc < corePoolSize)
            addWorker(null, true);
        else if (wc == 0)
            addWorker(null, false);
    }

在上篇博文中,其实我已经详细介绍它的作用了,就是在线程池启用初期,当工作线程数量没有达到指定数量时,每一次都会新建一个线程去执行任务。注意到这里的addWorker第一个参数都是null,意味着新线程都是从等待队列头获得任务。addWorker方法在上篇也已经详细讲过,不在重复了。

到这一步为止,schedule加入任务已经完成,接下来看看工作线程如何取任务并执行的。

3.3 Worker执行线程

在addWorker方法中,我们可以看到几条关键语句:

 private boolean addWorker(Runnable firstTask, boolean core) {
        ......
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
     				...
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
           ...
        }
        return workerStarted;
    }

新的工作线程就是Worker实例,这是定义在ThreadPoolExecutor里面的一个内部类,专门用作线程池工作线程。
此处t.start(),其实就是开始启动线程运行worker里面定义的run()方法。

我们摘取一下其中一些关键部分:

   private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
  		final Thread thread;
        Runnable firstTask;
        ......
 		public void run() {
            runWorker(this);
        }
}
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

runWorker()是线程池执行最关键的一个方法。这段代码实质就做了2件事:确定待执行的任务;然后执行任务。

3.3.1 确定待执行的任务

如何确定该执行哪个任务,代码里面有2条语句提供了2种途径:

  1. Runnable task = w.firstTask;
    firstTask其实是从Worker构造函数传递进来的。这条语句对于延时执行线程池其实无用,只针对其他3种线程池有效。这里的firstTask,仅仅在其他3种线程池启动初期,直接将任务交给新线程去处理的场景。延时执行线程池这里的firstTask都是null。

  2. while (task != null || (task = getTask()) != null)
    通过getTask()方法获得一个任务,实质是从等待队列中获取一个任务,主要源代码如下:

 private Runnable getTask() {
        ......
        for (;;) {
            ......
			boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
			......
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

这里是一个死循环,主要调用了2个方法:poll()和take()。通过timed定义可以知道,对于延时线程池,timed肯定是false,因此实际调用的是
workQueue.take();

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 <= 0)
                            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();
            }
        }

first = queue[0];可以看出,take()取到的是queue队列的第一个任务。当first为空的时候(等待队列没有任务待执行),available.await();被执行,线程进入休眠。上面代码讲过,加入新任务到队列时,会调用available.signal();发送信号,唤醒休眠中的线程。finishPoll()方法非常关键,我们放到下篇讲解。

3.3.2执行任务

执行从队列中取到的任务,其实就是一条语句:

	task.run();
 	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);
            }
        }

无论是不是周期性任务,都是使用这同一个run()方法。
忽略次要代码,核心就是2段代码:

  1. 如果不是周期性的任务
    执行ScheduledFutureTask.super.run()。这是调用父类FutureTask的run()方法,如下:
public void run() {
        ......
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                ......
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                   ......
                }
               ......
            }
        } finally {
        	......
        }
    }

其实关键就是这个call,执行的就是我们任务中定义的方法体。

  1. 如果是周期性的任务
    执行ScheduledFutureTask.super.runAndReset();这也是父类FutureTask的方法,和run()几乎一样,只不过不给执行结果,因为还需要周期性的执行。

setNextRunTime()根据period设置下次运行的时间:

	private void setNextRunTime() {
            long p = period;
            if (p > 0)
                time += p;
            else
                time = triggerTime(-p);
        }

reExecutePeriodic(outerTask);

    void reExecutePeriodic(RunnableScheduledFuture<?> task) {
        if (canRunInCurrentRunState(true)) {
            super.getQueue().add(task);
            if (!canRunInCurrentRunState(true) && remove(task))
                task.cancel(false);
            else
                ensurePrestart();
        }
    }

可以看到,super.getQueue().add(task);又被执行了,任务又被放回等待队列,等待下一次执行。

4、scheduleAtFixedRate接口方法实现

如果需要重复周期性的执行某个任务,可以使用scheduleAtFixedRate接口。

 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        ......
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(period));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

从源代码可以看出,代码和schedule基本一样,唯一区别在于ScheduledFutureTask多给了一个period参数。后面的机制基本一模一样,如何重复执行的,上面也讲过了,就是执行完之后放回阻塞队列,等待下一次执行。

5、总结

整个Scheduled线程池的源码分析基本都完成了,经过这次梳理,感觉我自己收获也挺大的。

通过本篇的分析,对延时执行线程池进行总结一下:

  1. 线程数量是定长的,不可以超过指定值。
  2. 队列用的延时队列,初始长度16,如果不够则自动扩容,每次扩大50%;需要小心使用,避免等待任务过多。
  3. 常用的是schedule(),延时执行某个任务;
  4. scheduleAtFixedRate也常用,适合执行重复性、周期性任务。
发布了5 篇原创文章 · 获赞 18 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zzmlake/article/details/104251292