学习线程池源码--ScheduledThreadPoolExecutor
1. 创建ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,实现了ScheduledExecutorService接口,在ThreadPoolExecutor的基础上增加了定时的功能,包括指定延时后执行任务和指定延时后执行任务。ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以多线程执行,所以ScheduledThreadPoolExecutor是比Timer更优的选择。
    public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
        public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
        }
    }
从构造函数可以看出ScheduledThreadPoolExecutor还是调用的父类ThreadPoolExecutor的构造方式,其中最大线程数固定为Integer.MAX_VALUE,空闲时间固定为0,工作队列固定为DelayedWorkQueue。DelayedWorkQueue是一个无界队列,故最大线程数并没有什么意义。
Executors工具类提供了几种ScheduledThreadPoolExecutor的创建方法:
1. newScheduledThreadPool
只指定核心线程数,使用默认的线程工厂以及拒绝策略,最多创建corePoolSize个线程。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
2. newSingleThreadScheduledExecutor
核心线程数为1,使用默认的线程工厂以及拒绝策略,单线程的newScheduledThreadPool。
    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
2. 核心思想
ScheduledThreadPoolExecutor的基本实现依赖于ThreadPoolExecutor,只不过用了特殊的工作队列DelayedWorkQueue(一种基于堆实现的优先队列,类似PriorityQueue),DelayedWorkQueue的队首始终为执行时间最早的一个任务,当worker线程调用DelayedWorkQueue.take()方法时,如果已有leader线程则当前worker线程会成为follower线程进入等待状态(leader/follower多线程网络模型,后面在介绍DelayedWorkQueue时有说明),如果不存在leader线程则当前worker线程会成为leader线程,如果当前时间小于执行时间,则leader线程会阻塞直到当前时间大于等于执行时间才将队首的任务返回,然后leader线程就执行该任务并交出leader权限提拔一个follower线程成为新的leader。如果是周期性任务,则执行后根据执行频率重新计算下次执行时间后再放入DelayedWorkQueue中。
3. 核心方法
1. schedule方法
schedule有两个重载方法,一个用于执行Runnable任务,一个用于执行Callable任务,command为要执行的任务,delay为延时执行的时间,unit为延时执行的时间单位。二者都是先将任务封装为一个RunnableScheduledFuture对象(实际上就是ScheduledFutureTask),然后放入DelayedWorkQueue队列中,等待时间线程池中的线程从队列中获取任务并执行。Callable与Runnable的区别在于,Callable可以通过调用FutureTask.get()来获取执行结果,不过该方法会阻塞主线程直到获得结果。
    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;
    }
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay,
                                           TimeUnit unit) {
        if (callable == null || unit == null)
            throw new NullPointerException();
        RunnableScheduledFuture<V> t = decorateTask(callable,
            new ScheduledFutureTask<V>(callable,
                                       triggerTime(delay, unit)));
        delayedExecute(t);
        return t;
    }
2. scheduleAtFixedRate方法
scheduleAtFixedRate用于执行固定频率(时间间隔)的任务,command为要执行的任务,initialDelay为第一次执行的延迟时间,period为后面每次执行的时间间隔,unit为时间单位。与schedule方法一样,scheduleAtFixedRate方法也是通过创建一个RunnableScheduledFuture对象(实际上就是ScheduledFutureTask),然后放入DelayedWorkQueue队列中,等待时间线程池中的线程从队列中获取任务并执行。区别在于首次调用scheduleAtFixedRate方法时会在initialDelay时间之后首次执行任务,之后每次根据当前执行时间延迟period时间执行,所以每次执行的时间大致是能确定的。
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (period <= 0)
            throw new IllegalArgumentException();
        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;
    }
3. scheduleWithFixedDelay方法
scheduleWithFixedDelay也是用于执行固定频率的任务,与scheduleAtFixedRate方法不同的是,scheduleWithFixedDelay是在任务执行完成之后的时间加上delay时间延迟执行的,所以每次执行的时间不是确定的(因为不知道任务要执行多久),但间隔是确定的。其他的同scheduleAtFixedRate方法。
    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;
    }
4. delayedExecute方法
因为ScheduledThreadPoolExecutor是用于执行定时或周期性任务的线程池,所以每次提交任务都是直接放入工作队列而不是直接执行,故每次提交任务会创建空的工作线程直到线程池中的线程数达到核心线程数,然后每个线程都是通过循环调用take()方法去队列中获取任务然后执行。
    private void delayedExecute(RunnableScheduledFuture<?> task) {
        //如果线程池已停止运行,则执行拒绝策略
        if (isShutdown())
            reject(task);
        else {
            //将任务加入工作队列
            super.getQueue().add(task);
            //再次确认线程池状态,如果是停止状态则将task从工作队列移除,然后取消任务但不中断执行线程
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                // 该方法在ThreadPoolExecutor中实现,是为了确保线程池中至少有一个线程启动,即使corePoolSize为0
                // 在这里是每次添加任务都会创建一个线程,直到线程池中的线程数达到核心线程数
                ensurePrestart();
        }
    }
4. 重要内部类
1. ScheduledFutureTask
ScheduledFutureTask继承自FutureTask实现了RunnableScheduledFuture接口,它的time属性代表了该任务的执行时间,sequenceNumber代表了该任务加入ScheduledThreadPoolExecutor的序号,period代表了任务执行的时间间隔。
    private class ScheduledFutureTask<V>
            extends FutureTask<V> implements RunnableScheduledFuture<V> {
        //添加到ScheduledThreadPoolExecutor中的序号
        private final long sequenceNumber;
        //这个任务要被执行的具体时间
        private long time;
        //任务执行的时间间隔
        private final long period;
        /** The actual task to be re-enqueued by reExecutePeriodic */
        RunnableScheduledFuture<V> outerTask = this;
        //在DelayedWorkQueue中的索引,方便快速取消任务
        int heapIndex;
    }
ScheduledFutureTask重写了compareTo方法,首先通过time来比较大小,如果time相同,则跟据sequenceNumber的大小来进行判断。这里主要是为了方便DelayedWorkQueue排序用。
		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;
        }
2. DelayedWorkQueue
       ScheduledThreadPoolExecutor之所以使用DelayedWorkQueue作为工作队列,是因为定时任务需要优先执行时间靠前的任务,所以ScheduledThreadPoolExecutor就在内部实现了一个线程安全的、阻塞的优先队列。
       DelayedWorkQueue是一个基于堆的数据结构,继承自AbstractQueue实现了BlockingQueue接口。内部使用数组实现堆的功能,通过Leader/Follower多线程网络模型防止了动态内存分布及线程间的数据交换。
       由于DelayedWorkQueue是优先队列,所以用最小堆实现的,即最早要执行的任务放在队列首部。
       Leader/Follower多线程网络模型:最多只有一个leader线程用于监听任务,而其他的空闲中的线程(follower)都在等待成为leader,当leader获取到任务之后会首先提拔一个follower线程成为leader,然后自己去执行任务成为processor。
    static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {
        //初始容量
        private static final int INITIAL_CAPACITY = 16;
        //底层用数组实现堆
        private RunnableScheduledFuture<?>[] queue =
            new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
        //使用可重入锁来保证线程安全
        private final ReentrantLock lock = new ReentrantLock();
        //队列大小
        private int size = 0;
        //leader线程,始终为最早要执行任务的执行线程
        private Thread leader = null;
        //与lock结合使用
        private final Condition available = lock.newCondition();
    }
1.入列方法(offer)
    public boolean offer(Runnable x) {
        if (x == null)
            throw new NullPointerException();
        RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
        //获取lock实例并加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int i = size;
            //如果容量不够,则进行扩容操作
            if (i >= queue.length)
                grow();
            //修改队列大小
            size = i + 1;
            //如果原来队列为空,则直接将任务放入数组索引0处
            if (i == 0) {
                queue[0] = e;
                setIndex(e, 0);
            }
            //否则根据堆的规则进行排序
            else {
                siftUp(i, e);
            }
            //如果入队的任务在队列首部,则重置leader线程
            if (queue[0] == e) {
                //先将leader线程置为空
                leader = null;
                //再重新提拔一个follower成为leader
                available.signal();
            }
        } finally {
            //释放锁
            lock.unlock();
        }
        return true;
    }
所有的入列方法都是调用这一个方法。首先判断队列容量是否足够,不够则进行扩容;然后根据最小堆的规则进行排序插入操作;最后判断加入的任务是否具有最高优先级,是则重置leader线程用于执行插入的任务。整个过程通过ReentrantLock加锁来保证线程安全。这里涉及了几个重要的方法,扩容方法grow,排序插入方法siftUp。
2. 扩容方法(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);
    }
与ArrayList的扩容方法很相似,DelayedWorkQueue容量不够时会自动扩容,每次扩容50%,但是不能超过Integer最大值,通过Arrays.copyOf进行扩容,实际上底层调用System.arraycopy()达到高效扩容的目的。
3. 排序插入方法(siftUp)
    private void siftUp(int k, RunnableScheduledFuture<?> key) {
        while (k > 0) {
            //取k位置的父节点索引
            int parent = (k - 1) >>> 1;
            //取父节点e
            RunnableScheduledFuture<?> e = queue[parent];
            //如果key比父节点大,则满足最小堆规则,停止循环
            if (key.compareTo(e) >= 0)
                break;
            //如果父节点较大,则把父节点位置与key交换
            queue[k] = e;
            setIndex(e, k);
            k = parent;
        }
        //通过循环确定了key的索引位置,将key插入该位置
        queue[k] = key;
        setIndex(key, k);
    }
每次先将插入的任务放在队尾,然后通过比较大小确定最终位置。因为ScheduledFutureTask已经根据时间大小重写了compareTo()方法,所以这里直接通过compareTo()方法比较大小来进行排序。这里需要明白最小堆的规则,需要满足2个条件:1.是完全二叉树,2.父节点的值不能小于子节点的值。最小堆在数组中的表示方法如下:
    // 对于n位置的节点来说:
    int left = 2 * n + 1; // 左子节点
    int right = 2 * n + 2; // 右子节点
    int parent = (n - 1) / 2; // 父节点,当然n要大于0,根节点是没有父节点的
4. 等待获取队首方法(take),实现任务调度的核心
    public RunnableScheduledFuture<?> take() throws InterruptedException {
        //获取lock实例并加锁
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                //获取队首任务
                RunnableScheduledFuture<?> first = queue[0];
                //如果队首任务为空,进入等待状态
                if (first == null)
                    available.await();
                else {
                    //获取队首任务的延时数
                    long delay = first.getDelay(NANOSECONDS);
                    //延时数小于等于0;说明任务已达调度时间,取出队列交.
                    给leader线程去执行。
                    if (delay <= 0)
                        return finishPoll(first);
                    //任务还未到执行时间,分情况进入等待状态
                    first = null; // don't retain ref while waiting
                    //如果leader线程已存在,则进入等待状态成为follower,等待提拔为leader线程
                    if (leader != null)
                        available.await();
                    else {
                        //如果leader线程不存在,则将当前线程提拔为leader线程
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            //进入等待状态直到达到任务执行时间
                            available.awaitNanos(delay);
                        } finally {
                            //到达执行时间,leader线程去执行任务,成为processor
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            //leader线程已成为processor,如果队首还有任务,则重新提拔一个follower线程成为leader线程
            if (leader == null && queue[0] != null)
                available.signal();
            //释放锁
            lock.unlock();
        }
    }
       take()方法充分体现了leader/follower多线程网络模型的思想以及ReentrantLock与Condition的配合运用,也是ScheduledThreadPoolExecutor实现任务调度的关键所在。
       首先通过ReentrantLock加锁,然后去取队首的任务(队首的任务由leader线程执行),如果已有leader线程则成为follower线程等待提拔为leader线程,如果没有leader线程则直接晋升为leader线程,等待达到执行时间后通过finishPoll()方法取出任务并执行,同时交出leader权限,提拔一个follower线程成为新的leader线程来执行下一个任务。
       其中的follower线程等待通过Condition.await()方法实现,leader线程的等待通过Condition.awaitNanos()实现,提拔一个follower线程成为新的leader线程通过Condition.signal()实现。
       DelayedWorkQueue出列的方法还有poll方法,不过线程池只用到了take()方法,take()方法中还有一个出列动作的方法finishPoll()。
5. 出列方法(finishPoll)
    private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
        //将队列大小-1并赋值给s
        int s = --size;
        //取队尾任务x
        RunnableScheduledFuture<?> x = queue[s];
        //将队尾任务置为空
        queue[s] = null;
        //如果队尾和队首不是同一个任务,则进行移除排序
        if (s != 0)
            siftDown(0, x);
        //将f在堆中的索引设为-1代表已取出,然后将任务返回
        setIndex(f, -1);
        return f;
    }
take()方法会阻塞线程直到任务达到执行时间,达到执行时间后就会调用finishPoll()方法将任务取出队列。取出的思路是将队尾的任务放到队首,然后通过siftDown()进行重排序。
6. 移除排序方法(siftDown)
    private void siftDown(int k, RunnableScheduledFuture<?> key) {
        //取队列长度的一半
        int half = size >>> 1;
        //通过循环比较保证父节点不大于子节点
        while (k < half) {
            //取k的左子节点作为child
            int child = (k << 1) + 1;
            RunnableScheduledFuture<?> c = queue[child];
            //取k的右子节点
            int right = child + 1;
            //如果右子节点在队列中并且左子节点大于右子节点,则取右子节点作为child
            //目的是为了取左右子节点中较小的一个作为child
            if (right < size && c.compareTo(queue[right]) > 0)
                c = queue[child = right];
            //将key与子节点中较小的一个进行比较,如果key较小,说明满足最小堆要求,终止循环
            if (key.compareTo(c) <= 0)
                break;
            //否则将key与child交换位置,然后继续向下检查
            queue[k] = c;
            setIndex(c, k);
            k = child;
        }
        queue[k] = key;
        setIndex(key, k);
    }
学习线程池源码--ScheduledThreadPoolExecutor的更多相关文章
- 学习线程池源码--ThreadPoolExecutor
		1 创建ThreadPoolExecutor ThreadPollExecutor有四个构造函数,但本质上都是调用这一个构造函数. public ThreadPoolExecutor(int core ... 
- java多线程——线程池源码分析(一)
		本文首发于cdream的个人博客,点击获得更好的阅读体验! 欢迎转载,转载请注明出处. 通常应用多线程技术时,我们并不会直接创建一个线程,因为系统启动一个新线程的成本是比较高的,涉及与操作系统的交互, ... 
- java多线程----线程池源码分析
		http://www.cnblogs.com/skywang12345/p/3509954.html 线程池示例 在分析线程池之前,先看一个简单的线程池示例. 1 import java.util.c ... 
- Java多线程学习之线程池源码详解
		0.使用线程池的必要性 在生产环境中,如果为每个任务分配一个线程,会造成许多问题: 线程生命周期的开销非常高.线程的创建和销毁都要付出代价.比如,线程的创建需要时间,延迟处理请求.如果请求的到达率非常 ... 
- 线程池之ScheduledThreadPoolExecutor线程池源码分析笔记
		1.ScheduledThreadPoolExecutor 整体结构剖析. 1.1类图介绍 根据上面类图图可以看到Executor其实是一个工具类,里面提供了好多静态方法,根据用户选择返回不同的线程池 ... 
- Java线程池源码解析
		线程池 假如没有线程池,当存在较多的并发任务的时候,每执行一次任务,系统就要创建一个线程,任务完成后进行销毁,一旦并发任务过多,频繁的创建和销毁线程将会大大降低系统的效率.线程池能够对线程进行统一的分 ... 
- Java并发编程中线程池源码分析及使用
		当Java处理高并发的时候,线程数量特别的多的时候,而且每个线程都是执行很短的时间就结束了,频繁创建线程和销毁线程需要占用很多系统的资源和时间,会降低系统的工作效率. 参考http://www.cnb ... 
- 【图灵学院10】高并发之java线程池源码分析
		1. 提纲 1)线程池的模块结构 2)示例&原理解析 2. 问题 1)线程池包含哪些东西 2)线程池的运作原理 3)调度线程池的运作原理 4)线程池怎么实现FixRate,FixDelay,他 ... 
- 手撕ThreadPoolExecutor线程池源码
		这篇文章对ThreadPoolExecutor创建的线程池如何操作线程的生命周期通过源码的方式进行详细解析.通过对execute方法.addWorker方法.Worker类.runWorker方法.g ... 
随机推荐
- SpringCloud搭建分布式配置中心(基于git)
			1.简介 Spring Cloud Config.它用来为分布式系统中的基础设施和微服务提供集中化的外部配置支持,分为服务端和客户端两个部分. 其中服务端也称为分布式配置中心,他是独立的微服务应用,用 ... 
- 84 落单的数 III
			原题网址:http://www.lintcode.com/zh-cn/problem/single-number-iii/# 给出2*n + 2个的数字,除其中两个数字之外其他每个数字均出现两次,找到 ... 
- PAT甲级——A1099 Build A Binary Search Tree
			A Binary Search Tree (BST) is recursively defined as a binary tree which has the following propertie ... 
- mysql 根据时间查询 格式转换竟然要注意大小写,天坑
			时间需要转换格式在查询 查询2018年12月24日以后的记录 SELECT id FROM t_client_info WHERE DATE_FORMAT(visit_datetime,'%Y-%m- ... 
- IntelliJ IDEA community 安装教程
			jetbrains官网下载 IntelliJ IDEA安装包 https://www.jetbrains.com/idea/download/#section=windows 此处选择社区版的zip文 ... 
- css3之背景background-origin,background-clip,background-size
			background-origin属性指定了背景图像的位置区域. content-box, padding-box,和 border-box区域内可以放置背景图像. background-clip用来 ... 
- CAS企业级单点登录原理
			https://blog.csdn.net/anumbrella/article/details/80821486 1. 单点登录概述 1.1. 什么是单点登录? 单点登录:Single Sign O ... 
- Luogu P4933 大师(dp)
			P4933 大师 题意 题目背景 建筑大师最近在跟着数学大师ljt12138学数学,今天他学了等差数列,ljt12138决定给他留一道练习题. 题目描述 ljt12138首先建了\(n\)个特斯拉电磁 ... 
- IDEA2018激活码
			请复制如下内容到文本编辑器(如notepad++)把博客的内容去掉 N757JE0KCT-eyJsaWNlbnNlSWQiOiJONzU3SkUwS0NUIiwibGljZW5zZWVOYW1lIjo ... 
- Tengine 如何查找 server 块
			概述 本文的目标读者是Tengine/Nginx 研发或者运维同学,如果自己对这块逻辑非常清楚,那可以略过,如果在配置或者开发 Tengine/Nginx 过程中,有如下疑问的同学,本文或许能解答你多 ... 
