【Java并发学习四】如何实现一个定时线程池

【Java并发学习一】如何实现一个线程池上进行扩展,我们想一下如何实现一个定时任务线程池呢?

所谓 “定时任务线程池” 就是指放入线程池的任务,可以按照指定的等待周期循环执行。

Java里面ScheduledThreadPoolExecutor这个类实现了这种功能。Spring里面的定时任务也是在ScheduledThreadPoolExecutor的基础上扩展而来。

如何实现

我们先回顾下【Java并发学习一】如何实现一个线程池中实现的自定义线程池:

image.png

如图,放入线程池的任务,在线程数超过corePoolSize的情况下会放入队列,而线程池内的线程则不断从队列中读取任务消费。

如果我们想要一个放入的任务每隔一段时间(如一小时)定时执行,似乎挺简单:

  1. 消费完的任务,需要再放进队列中被消费,
  2. 线程池中线程取任务的时间不能是马上,得等待一小时后才消费。

第一点不难,关键是第二点如何实现。思考下,应该不能从线程下手,因为每个任务定时时间是不同的,线程消费时是不好控制的。那就只有从队列下手了。

我们将放入的任务增加一个delay延迟字段,然后使它被取出时,等待delay这么长就行。

OK,看到这里,你已经把定时任务线程池的原理理解的差不多啦~ 接下来我们看具体实现细节。

实现细节

1. 延迟队列的实现

最难的地方也就是 延迟队列 的实现。我们借鉴下已有的实现,在Java里面查找了下已有队列,果然发现一个DelayQueue的类。研究一番后发现,延迟队列需要用到一种 叫做 “” 的数据结构。

堆其实就是一个完全二叉树,延迟队列中用的是 最小堆(父结点<=子结点)。一般用数组来存储,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2i + 12i + 2

  • 堆数组尾部添加元素时,需要不停将该元素与其父结点进行对比交换,类似于元素在“上升”。也就是使新添加的元素插入一个有序的序列中,形成一个新的有序堆序列
  • 堆删除元素时,总是先删除根结点,然后将最后一个元素移到根结点,与子结点对比交换,类似于元素在“下沉”。最终形成新的有序堆序列。

关于堆的更多细节可以自行百度谷歌,或者查看我之前排序算法总结里面的堆排序: 数据结构基础(六)排序

自定义延迟队列

为什么延迟队列用堆实现呢?
     因为堆中元素是有顺序的,延迟队列中排序是以 任务的等待周期 比较,这样保证了从延迟队列中取出元素(即堆中获取头节点),总是取出的等待周期最小的任务消费。

   /**
     * 定义任务的compareTo方法,用于堆插入或者删除时的堆排序
     */
    @Override
    public int compareTo(Delayed o) {
        MyScheduledFutureTask x = (MyScheduledFutureTask)o;
        long diff = time - x.time;
        if (diff < 0)
            return -1;
        else if (diff > 0)
            return 1;
        else
            return 1;
    }

那延迟队列的延迟如何实现呢?
    延迟队列用Condition.awaitNanos(delay)条件变量来实现了线程的等待。我们看下延迟队列的成员变量,queue 就是一个堆结构的任务数组;lock锁保证的队列新增和删除时的线程安全;lockConditionleader(领导线程) 一起,控制线程的等待和唤醒。具体细节看下面。

/**
 * 自定义延迟队列
 */
public class MyDelayQueue extends AbstractQueue implements BlockingQueue{
    
    /** 堆数据结构 */
    private MyScheduledFutureTask[] queue = new MyScheduledFutureTask[16];

    /** 队列元素个数 */
    private int size = 0;

    /** 锁,用于队列新增和删除时保持线程安全 */
    private final transient ReentrantLock lock = new ReentrantLock();

    /** 用于实现队列延迟取出元素 */
    private final Condition available = lock.newCondition();

    /**
     * 领导线程,可理解为正在获取节点的线程
     * 和锁、Condition一起,
     * 控制队列延迟获取节点时,线程的等待和唤醒
     */
    private Thread leader = null;

     ………………
}

延迟队列中添加任务是怎样的?
     延迟队列中添加任务很简单,直接往堆尾部增加节点,然后执行 “上升”操作来重排序即可

public boolean add(Object o) {
        lock.lock();

        try {
            //队列空间不足时,扩展
            if(size >= queue.length-1){
                queue = Arrays.copyOf(queue, queue.length*2);
            }

            MyScheduledFutureTask task = (MyScheduledFutureTask) o;
            //queue没有任务时,直接往数组第一个放入任务
            if(size == 0){
                queue[size++] = task;
            }
            //queue已经有任务时,在堆尾部增加任务,并实行堆上浮操作
            else {
                size++;
                siftUp(size-1, task);
            }
        } finally {
            lock.unlock();
        }
        return true;
    }

重点!!!:延迟队列中取出任务是怎样的?
     延迟队列中获取并删除任务比较复杂,因为线程池中多个线程同时在从延迟队列中取任务,所以需要用lockConditionleader(领导线程) 一起,控制当一个线程在取任务,其余线程阻塞,等到该任务获取完毕,再唤醒其余线程。

重点是leader的理解,leader可理解为正在获取节点的线程。当leader为空时,证明没有线程在从队列中获取节点,该线程可自己成为leader获取任务节点;当leader不为空时,证明有线程正在获取节点,此时的leader在堵塞倒计时中(awaitNanos(delay)),故该线程需要阻塞;当线程取元素结束时,都需要唤醒Condition等待上的任一线程。

public MyScheduledFutureTask take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            for (;;) {
                //取出堆中的头节点
                MyScheduledFutureTask first = queue[0];
                //如果堆中没有节点,则挂起线程
                if (first == null)
                    available.await();
                else {
                    //获取节点任务的等待时间
                    long delay = first.getDelay(NANOSECONDS);
                    //如果已经不需要等待,直接返回节点任务,并将堆中尾节点视为头节点进行堆下沉排序
                    if (delay <= 0)
                        return finishPoll(first);
                    //为下面代码线程等待时不再持有无用的first对象,直接释放它
                    first = null;

                    //leader不为空,则某个awaitNanos线程已经在取任务,挂起线程
                    if (leader != null)
                        available.await();
                    //leader为空,此时没有线程在取任务
                    else {
                        //设置leader为当前线程
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        //调用awaitNanos方法等待固定时间后,等待其他线程的唤醒
                        try {
                            available.awaitNanos(delay);
                        } finally {
                            //leader线程被唤醒,下个循环将返回节点,此时将leader设置为null
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            //队列非空时,unlock前 随机唤醒等待条件上的任一队列
            if (queue[0] != null)
                available.signal();
            lock.unlock();
        }
    }

   /**
     * 删除f节点,将堆中尾节点设置为头节点,然后进行下沉排序
     */
    private MyScheduledFutureTask finishPoll(MyScheduledFutureTask f) {
        int s = --size;
        MyScheduledFutureTask x = queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);
        return f;
    }

最后贴出完整的自定义延迟队列代码:

/**
 * 自定义延迟队列
 */
public class MyDelayQueue extends AbstractQueue implements BlockingQueue{
    
    /** 堆数据结构 */
    private MyScheduledFutureTask[] queue = new MyScheduledFutureTask[16];

    /** 队列元素个数 */
    private int size = 0;

    /** 锁,用于队列新增和删除时保持线程安全 */
    private final transient ReentrantLock lock = new ReentrantLock();

    /** 用于实现队列延迟取出元素 */
    private final Condition available = lock.newCondition();

    /**
     * 领导线程,可理解为正在获取节点的线程
     * 和锁、Condition一起,
     * 控制队列延迟获取节点时,线程的等待和唤醒
     */
    private Thread leader = null;

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean add(Object o) {
        lock.lock();

        try {
            //队列空间不足时,扩展
            if(size >= queue.length-1){
                queue = Arrays.copyOf(queue, queue.length*2);
            }

            MyScheduledFutureTask task = (MyScheduledFutureTask) o;
            //queue没有任务时,直接往数组第一个放入任务
            if(size == 0){
                queue[size++] = task;
            }
            //queue已经有任务时,在堆尾部增加任务,并实行堆上浮操作
            else {
                size++;
                siftUp(size-1, task);
            }
        } finally {
            lock.unlock();
        }
        return true;
    }

    @Override
    public MyScheduledFutureTask take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            for (;;) {
                //取出堆中的头节点
                MyScheduledFutureTask first = queue[0];
                //如果堆中没有节点,则挂起线程
                if (first == null)
                    available.await();
                else {
                    //获取节点任务的等待时间
                    long delay = first.getDelay(NANOSECONDS);
                    //如果已经不需要等待,直接返回节点任务,并将下一个节点视为头节点进行堆排序
                    if (delay <= 0)
                        return finishPoll(first);
                    //下面代码线程等待时不再持有无用的first对象,直接释放它
                    first = null;

                    //leader不为空,则某个awaitNanos线程已经在取任务,挂起线程
                    if (leader != null)
                        available.await();
                    //leader为空,此时没有线程在取任务
                    else {
                        //设置leader为当前线程
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        //调用awaitNanos方法等待固定时间后,将被唤醒
                        try {
                            available.awaitNanos(delay);
                        } finally {
                            //任务等待完毕,leader线程被唤醒,下个循环将返回节点,此时将leader设置为null
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            //队列非空时,unlock前 随机唤醒等待条件上的任一队列
            if (queue[0] != null)
                available.signal();
            lock.unlock();
        }
    }

    /**
     * 删除f节点,将堆中尾节点设置为头节点,然后进行下沉排序
     */
    private MyScheduledFutureTask finishPoll(MyScheduledFutureTask f) {
        int s = --size;
        MyScheduledFutureTask x = queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);
        return f;
    }

    /**
     * 在堆尾部增加节点,实行堆排序的上浮操作
     */
    private void siftUp(int k, MyScheduledFutureTask key) {
        //如果子节点比父节点大,则替换
        while (k > 0) {
            int parent = (k - 1) / 2;
            MyScheduledFutureTask e = queue[parent];
            if (key.compareTo(e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }

        queue[k] = key;
    }

    /**
     * 从堆的顶部拿取节点,实现堆排序的下沉操作
     */
    private void siftDown(int k, MyScheduledFutureTask key) {
        int half = size / 2;
        while (k < half) {
            int child = (k*2) + 1;
            MyScheduledFutureTask c = queue[child];
            int right = child + 1;
            if (right < size && c.compareTo(queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo(c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }

        queue[k] = key;
    }

    
    //…………其余方法略
}

2. 线程任务的实现

Runnable的包装类,记录每个任务执行时的等待周期period和下个周期任务应该触发的时间time,以及run()结束前,将任务再次放入队列中

/**
 * 定时任务执行类
 */
public class MyScheduledFutureTask
        implements Runnable, Delayed {
    
    /** 任务触发时间的纳秒值 */
    private long time;
    
    /** 循环间隔的纳秒值 */
    private final long period;
    
    /** 线程池中的队列 */
    private BlockingQueue queue;

    /** 执行任务 */
    private Runnable task;

    public MyScheduledFutureTask(Runnable r, long time, int period, BlockingQueue<Runnable> queue) {
        this.task = r;
        this.time = time;
        this.period = period;
        this.queue = queue;
    }

    /**
     * 自定义任务队列实现了堆数据结构,此方法用于堆插入或者删除时的堆排序
     */
    @Override
    public int compareTo(Delayed o) {
        MyScheduledFutureTask x = (MyScheduledFutureTask)o;
        long diff = time - x.time;
        if (diff < 0)
            return -1;
        else if (diff > 0)
            return 1;
        else
            return 1;
    }

    /**
     *  获取触发时间与当前时间的时间差
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(time - System.nanoTime(), NANOSECONDS);
    }

    @Override
    public void run() {
        //执行逻辑
        task.run();
        //任务执行结束后,将下次任务触发的时间增加一周期
        time += TimeUnit.SECONDS.toNanos((long)period);
        //重新往线程池队列中加入此任务
        queue.add(this);
    }


}

3. 定时线程池的实现

有了上面的基础,定时线程池的实现就很简单了。scheduleAtFixedRate()方法中,将执行的任务command封装为包装类MyScheduledFutureTask;然后放入延迟队列中;最后调用ensurePrestart()方法往线程池放入一个空任务,使线程池创建线程开始不断读取队列中的任务执行。

/**
 * 自定义简单定时线程池
 */
public class MyScheduledThreadPool extends  MyThreadPool{

    public MyScheduledThreadPool(int initPoolNum) {
        super(initPoolNum, Integer.MAX_VALUE, new MyDelayQueue());
    }


    /**
     * 每隔固定时间周期执行任务
     * @param command 任务
     * @param period 时间周期(以秒为单位)
     */
    public void scheduleAtFixedRate(Runnable command, int period) {
        if (command == null)
            throw new NullPointerException();

        if (period <= 0)
            throw new IllegalArgumentException();

        //包装任务为周期任务
        MyScheduledFutureTask mScheduledTask =
                new MyScheduledFutureTask(command, triggerTime(period), period, getTaskQueue());
        //延迟周期执行
        delayedExecute(mScheduledTask);
    }

    private void delayedExecute(MyScheduledFutureTask task) {
        getTaskQueue().add(task);
        ensurePrestart();
    }


    /**
     * 确保线程池已经启动,有线程会去读取队列,并执行任务
     */
    void ensurePrestart() {
        execute(null);
    }

    /**
     * 获取触发时间
     * @param period 延迟时间
     * @return 返回触发时间
     */
    long triggerTime(int period) {

        return System.nanoTime() + TimeUnit.SECONDS.toNanos((long)period);
    }
}

/**
 * 自定义简单线程池
 */
public class MyThreadPool{
    /**存放线程的集合*/
    private ArrayList<MyWorkThread> threads;
    /**任务队列*/
    private BlockingQueue<Runnable> taskQueue;
    /**线程池初始限定大小*/
    private int threadNum;
    /** 线程池最大大小 */
    private int maxThreadNum;
    /**已经工作的线程数目*/
    private int workThreadNum;

    private final ReentrantLock mainLock = new ReentrantLock();

    public MyThreadPool(int initPoolNum) {
        this.threadNum = initPoolNum;
        maxThreadNum = initPoolNum;
        this.threads = new ArrayList<>(initPoolNum);
        //任务队列初始化为线程池线程数的四倍
        this.taskQueue = new ArrayBlockingQueue<>(initPoolNum*4);

        this.workThreadNum = 0;
    }

    public MyThreadPool(int initPoolNum, int maxThreadNum, BlockingQueue<Runnable> taskQueue) {
        this.threadNum = initPoolNum;
        this.maxThreadNum = maxThreadNum;
        this.threads = new ArrayList<>(initPoolNum);
        //任务队列初始化为线程池线程数的四倍
        this.taskQueue = taskQueue;

        this.workThreadNum = 0;
    }

    public void execute(Runnable runnable) {
        try {
            mainLock.lock();
            //线程池未满,每加入一个任务则开启一个线程
            if(workThreadNum < threadNum) {
                MyWorkThread myThead = new MyWorkThread(runnable);
                myThead.start();
                threads.add(myThead);
                workThreadNum++;
            }
            //线程池已满,放入任务队列,等待有空闲线程时执行
            else {
                //队列已满,无法添加、且线程数小于最大线程数时,新增一个任务线程跑任务
                if(!taskQueue.offer(runnable) && workThreadNum < maxThreadNum) {
                    MyWorkThread overLimitThead = new MyWorkThread(runnable);
                    overLimitThead.start();
                    threads.add(overLimitThead);
                    workThreadNum++;
                }

                //队列已满,无法添加、且线程数大于等于最大线程数时,拒绝任务
                else{
                    rejectTask();
                }
            }
        } finally {
            mainLock.unlock();
        }
    }

    public BlockingQueue<Runnable> getTaskQueue() {
        return taskQueue;
    }

    private void rejectTask() {
        System.out.println("任务队列已满,无法继续添加,请扩大您的初始化线程池!");
    }

    public static void main(String[] args) {
        MyThreadPool myThreadPool = new MyThreadPool(5);
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行中");
            }
        };

        for (int i = 0; i < 20; i++) {
            myThreadPool.execute(task);
        }
    }

    /**
     * 自定义线程类
     */
    class MyWorkThread extends Thread{
        private Runnable task;

        public MyWorkThread(Runnable runnable) {
            this.task = runnable;
        }
        @Override
        public void run() {
            //该线程一直启动着,不断从任务队列取出任务执行
            while (true) {
                //如果初始化任务不为空,则执行初始化任务
                if(task != null) {
                    task.run();
                    task = null;
                }
                //否则去任务队列取任务并执行
                else {
                    Runnable queueTask = null;
                    try {
                        queueTask = taskQueue.take();
                        queueTask.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

补充:Java中定时线程池的实现

看完上面的内容,你其实已经对Java中ScheduledThreadPoolExecutor的实现原理了解的差不多了。

这里只补充下ScheduledThreadPoolExecutor的定时执行任务的四种方法差别:

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{…………}

前两个schedule方法是延迟执行任务,且只执行一次不循环。它们的区别在于形参中的任务类型不同,一个是Runnable,一个是Callable,其实Runnable最后也被包装成了Callable类型。

只执行一次的实现是:将任务(ScheduledFutureTask)的成员变量period设置为0,当period为0时任务运行结束不再往队列中重新加入。

后两个方法是周期执行任务,initialDelay形参指明任务第一次执行时的延迟时间。它们的差别在于,scheduleAtFixedRate是严格按照周期period执行任务,例如任务每隔四秒执行一次。scheduleWithFixedDelay是从任务结束起开始计时执行任务,例如任务运行完成后,再隔四秒执行一次。

它们的实现原理是:将任务(ScheduledFutureTask)的成员变量period设置为正数时,代表fixed-rate方式执行;设置为负数时,代表fixed-delay方式执行任务,。具体体现在:方法setNextRunTime()(设置下一次任务执行时间的)中,根据period的不同,计算方式不同:

private void setNextRunTime() {
            long p = period;
            //fixed-rate方式,直接在上一次执行任务的time上加上周期
            if (p > 0)
                time += p;
            //fixed-delay方式,在现在时间now()上加上周期
            else
                time = triggerTime(-p);
        }

long triggerTime(long delay) {
        return now() +
            ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
    }

参考文章:
深入理解Java线程池
线程池原理(四):ScheduledThreadPoolExecutor

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容