ScheduledThreadPoolExecutor定时任务线程池执行原理分析

一、示例代码

 

@Slf4j
public class ScheduleThreadPoolTest {
    private static ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
    int nCnt = 0;
    public void testScheduleThread(){
        log.debug(" fixed time--> start." );

        executor.scheduleWithFixedDelay(()->{
            log.debug(" fixed time--> nCnt:" + (nCnt++));
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },3000,2000,TimeUnit.MILLISECONDS);
        try {
            executor.awaitTermination(100000,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug(" fixed time--> end." );
    }

    public static void main(String[] args) {
        ScheduleThreadPoolTest scheduleThreadPoolTest = new ScheduleThreadPoolTest();
        scheduleThreadPoolTest.testScheduleThread();
    }
}

二、通用线程池ThreadPoolExecutor执行原理

1.构造函数和成员变量定义

corePoolSize

线程池的基本大小,即在没有任务需要执行的时候线程池的大小并且只有在工作队列满了的情况下才会创建超出这个数量的线程。这里需要注意的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。再考虑到keepAliveTime和allowCoreThreadTimeOut超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是corePoolSize。

maximumPoolSize

线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。这里值得一提的是largestPoolSize,该变量记录了线程池在整个生命周期中曾经出现的最大线程个数。为什么说是曾经呢?因为线程池创建之后,可以调用setMaximumPoolSize()改变运行的最大线程的数目。

poolSize

线程池中当前线程的数量,当该值为0的时候,意味着没有任何线程,线程池会终止;同一时刻,poolSize不会超过maximumPoolSize。

private final BlockingQueue<Runnable> workQueue; 阻塞任务队列。
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

新提交一个任务时的处理流程很明显:

1、如果当前线程池的线程数还没有达到基本大小(poolSize < corePoolSize),无论是否有空闲的线程新增一个线程处理新提交的任务;

2、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列未满时,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);

3、如果当前线程池的线程数大于或等于基本大小(poolSize >= corePoolSize) 且任务队列满时

3.1、当前poolSize<maximumPoolSize,那么就新增线程来处理任务;

3.2、当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于线程池的饱和策略RejectedExecutionHandler。

2.提交任务时,创建ThreadPoolExecutor的Worker类对象(实现runnable接口),并运行此线程,把此worker添加到works中。

   private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {

        final Thread thread;

        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        public void run() {
            runWorker(this);
        }

3.添加WORK的代码和流程,会新建一个worker对象,并且运行线程。

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

4.循环从阻塞任务队列取出任务,然后执行任务。

 final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); 
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } finally {
                        afterExecute(task, thrown);
                    }
                } 
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

 5.取任务的过程

  private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

三、ScheduledThreadPoolExecutor从队列取任务和存任务的过程

1.DelayedWorkQueue为ScheduledThreadPoolExecutor的阻塞任务队列。
 static class DelayedWorkQueue extends AbstractQueue<Runnable> implements BlockingQueue<Runnable> {
        private static final int INITIAL_CAPACITY = 16;
        private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture[16];
        private final ReentrantLock lock = new ReentrantLock();
        private int size = 0;
        private Thread leader = null;
        private final Condition available;

        DelayedWorkQueue() {
            this.available = this.lock.newCondition();
        }

2.取任务的过程

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
				// 如果此时等待根节点的leader线程不为空则通知其他线程继续等待
                if (leader != null)
                    available.await();
                else {
					// 如果此时leader线程为空,则把当前线程置为leader
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
						// 当前线程等待延迟的时间
                        available.awaitNanos(delay); 
                    } finally {
						// 延迟时间已到 则把当前线程变成非leader线程
						// 当前线程继续用于执行for循环的逻辑
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
} finally {
	// 如果leader为null 则唤醒一个线程成为leader
        if (leader == null && queue[0] != null)
            available.signal();
        lock.unlock();
    }
}

3. finishPoll(RunnableScheduledFuture)-获取根节点后重排序

private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
	// 因为取出根节点 所以队列深度减1 并赋值给s
    int s = --size;
	// 获取队列最后一个任务
    RunnableScheduledFuture<?> x = queue[s];
	queue[s] = null; // 该位置元素置空
	// 如果s已经根节点则直接返回,否则堆重排序
    if (s != 0)
        siftDown(0, x);
	// 取出来的任务 设置其堆索引为-1
    setIndex(f, -1);
    return f; // 返回任务
}

4.siftDown(int,RunnableScheduledFuture)-移除元素后重排序

private void siftDown(int k, RunnableScheduledFuture<?> key) {
	// 取队列当前深度的一半 相当于size / 2
    int half = size >>> 1;
    // 索引k(初值为0)的值大于half时 退出循环
    while (k < half) {
	// 获取左节点的索引
        int child = (k << 1) + 1;
		// 获取左节点的任务
        RunnableScheduledFuture<?> c = queue[child];
		// 获取右节点的索引
        int right = child + 1;
		// 如果右节点在范围内 且 左节点大于右节点,
        if (right < size && c.compareTo(queue[right]) > 0)
			// 更新child的值为右节点索引值 且更新c为右节点的任务
            c = queue[child = right];
		// 如果任务key小于任务c 则退出循环(最小堆)
        if (key.compareTo(c) <= 0)
            break;
		// 否则把任务c放到k上(较小的任务放到父节点上)
        queue[k] = c;
		// 设置任务c的堆索引
        setIndex(c, k);
		// 更新k的值为child
        k = child;
    }
    // 任务key插入k的位置
    queue[k] = key;
    // 设置任务key的堆索引k
    setIndex(key, k);
}

执行的流程图为:

5.入队列的过程  offer(Runnable)-新增元素

public boolean offer(Runnable x) {
    if (x == null)
        throw new NullPointerException();
	// 只能存放RunnableScheduledFuture任务
    RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
    // 为了保证队列的线程安全,offer()方法为线程安全方法
	final ReentrantLock lock = this.lock;
    lock.lock();
	try {
	// 当前队列实际深度,即队列中任务个数
        int i = size;
		// 如果任务数已经超过数组长度,则扩容为原来的1.5倍
        if (i >= queue.length)
            grow();
		// 队列实际深度+1
        size = i + 1;
		// 如果是空队列 新增任务插入到数组头部;
        if (i == 0) {
            queue[0] = e;
			// 设置该任务在堆中的索引,便于后续取消或者删除任务;免于查找
            setIndex(e, 0);
        } else {
			// 如果不是空队列 则调用siftUp()插入任务
            siftUp(i, e);
        }
		// 如果作为首个任务插入到数组头部
        if (queue[0] == e) {
			// 置空当前leader线程
            leader = null;
			// 唤醒一个等待的线程 使其成为leader线程
            available.signal();
        }
    } finally {
        lock.unlock();
    }
    return true;
}

 这个方法理解的难点在于leader线程。若新增任务插入空队列中,首先清空leader线程,并唤醒等待线程中的某一个线程,把唤醒的线程作为leader线程;若新增任务插入前,队列中已经存在任务,则说明已经有leader线程在等待获取根节点,此时无需设置leader线程。leader线程的作用就是用来监听队列的根节点任务,如果leader线程没有获取到根节点任务则通知其他线程等待,这表明leader线程决定着等待线程的状态。
用leader-before这种机制,可以减少线程的等待时间,而每一个等待的线程都有可能成为leader线程。注意:这里还不太清除哪些线程会等待。

6.siftUp(int,RunnableScheduledFuture)-新增任务后重排
新增任务插入队列(数组),首先插入到数组的尾部,然后对比其与该位置的父节点的大小,如果新增任务大于父节点任务(此处是最小堆),则新增任务位置不变,否则改变其与父节点的位置,并再比较父节点与父父节点的大小,直到根节点。插入的过程可以结合上面堆的二叉树变化过程图一起理解。
插入流程图:

private void siftUp(int k, RunnableScheduledFuture<?> key) {
	// 循环,当k为根节点时结束循环
	while (k > 0) {
		// 获取k的父节点索引,相当于(k-1)/2
        int parent = (k - 1) >>> 1;
		// 获取父节点位置的任务
        RunnableScheduledFuture<?> e = queue[parent];
		// 判断key任务与父节点任务time属性的大小,即延迟时间
        if (key.compareTo(e) >= 0)
            break; // 父节点任务延迟时间小于key任务延迟时间,则退出循环
        // 否则交换父节点parent与节点k的任务
		queue[k] = e;
		// 更新任务e在堆中的索引值
        setIndex(e, k);
		// 更新k的值 比较其与父父节点的大小
        k = parent;
	}
	// 任务key放入数组k的位置,k的值是不断更新的
	queue[k] = key;
	// 设置任务key在堆中的索引值
    setIndex(key, k);
}

7.scheduleWithFixedDelay方法:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (delay <= 0)
            throw new IllegalArgumentException();
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(-delay));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
}

任务添加到队列后,工作线程会从队列获取并移除到期的元素,然后执行run方法,所以下面看看ScheduledFutureTask的run方法如何实现定时调度的。

其中ScheduledFutureTask封装定时任务内部类,重点关注其run方法。

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

public void run() {
            boolean periodic = isPeriodic();
            if (!canRunInCurrentRunState(periodic))
                cancel(false);
            //仅执行一次
            else if (!periodic)
                ScheduledFutureTask.super.run();
            //定时任务
            else if (ScheduledFutureTask.super.runAndReset()) {
                setNextRunTime();
                //重新加入该任务到delay队列
                reExecutePeriodic(outerTask);
            }
        }

定时调度是先从队列获取任务然后执行,然后在重新设置任务时间,在把任务放入队列实现的。

如果任务执行时间大于delay时间则等任务执行完毕后的delay时间后在次调用任务,不会同一个任务并发执行。

四、上面的delayWorkQueue使用了堆的数据结构,

堆的一些属性
堆都是满二叉树.因为满二叉树会充分利用数组的内存空间;
最小堆是指父节点比左节点和右节点都小的结构,所以整个最小堆中,根节点是最小的节点;
最大堆是指父节点比左节点和右节点都大的结构,所以整个最大堆中,根节点是最大的节点;
最大堆和最小堆的左节点和右节点没有关系,只能判断父节点和左右两节点的大小关系;
基于堆的这些属性,堆适用于找到集合中的最大或者最小值;另外,堆结构记录任务及其索引的关系,便于插入数据或者删除数据后重新排序,所以堆适用于优先队列。

参考链接:https://blog.csdn.net/nobody_1/article/details/99684009

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值