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

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

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

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

如何实现

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

【Java并发学习四】如何实现一个定时线程池_第1张图片
image.png

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

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

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

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

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

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

实现细节

1. 延迟队列的实现

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

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

【Java并发学习四】如何实现一个定时线程池_第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 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 threads;
    /**任务队列*/
    private BlockingQueue 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 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 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  ScheduledFuture schedule(Callable 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

你可能感兴趣的:(【Java并发学习四】如何实现一个定时线程池)