ScheduledThreadPoolExecutor部分源码学习

ScheduledThreadPoolExecutor有一个功能,就是可以指定执行间隔,周期性的去执行指定的任务,和timer、定时任务的作用类似,网上也有好多说ScheduledThreadPoolExecutor的性能要比timer高,这个我还没有研究到,所以就不做对比,这篇笔记,主要记录部分源码的学习

类结构

ScheduledThreadPoolExecutor

在这里插入图片描述
可以看到 ScheduledThreadPoolExecutor继承了线程池ThreadPoolExecutor,除此之外,还实现了ScheduledExecutorService,说明了这个类至少是一个线程池,除此之外,在线程池之上,还有一些其他的功能

ScheduledExecutorService

我们可以看到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);

}

这四个方法的前两个不说了,就是开启一个延迟的任务,只会执行一次
下面这两个都是开启延迟定时周期性执行任务的,下面再具体研究

DelayedWorkQueue

在这里插入图片描述
这个类的作用,我觉得可以理解为:DelayedWorkQueue = DelayQueue + PriorityQueue 关于这三个类的笔记,我想单独再写一篇笔记来记录,所以这里不做过多的讨论,
我们可以认为这个内部类完成的操作就是:在将定时任务的元素插入到队列中的时候,会进行优先级排序,将最先执行的任务放到前面
比如:A任务5S之后执行,这时候插入了一个B任务,B任务需要在2S之后执行,那就把B插入到A前面

ScheduledFutureTask

在这里插入图片描述

这个类的作用,我目前的理解是:对我们指定的要执行的任务进行了一层包装

源码

公共方法一、triggerTime(delay, unit)

这个方法是在当前时间的基础之上 加上delay的时间,作为该任务执行的时间

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

公共方法二、decorateTask

这个方法主要是对指定的task进行一层再包装,包装成ScheduledFutureTask对象

公共方法三、delayedExecute

这个方法是为了将包装好的任务,添加到任务队列中

/**
 * 这个方法是把任务添加到队列中,在scheduledThreadPoolExecutor中,这里的队列是优先级队列
 * 1.添加到任务队列之前,会判断下线程池的状态,如果是非运行状态,就执行拒绝策略
 * 2.在添加到任务队列之后,如果线程池是shutdown状态,就remove,并且将任务取消
 * 3.如果不需要取消任务,就执行ensurePrestart(); 在该方法中,会判添加一个空的worker任务,去执行队列中的任务
 * @param task the task
 */
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();
	}
}


/**
 * Same as prestartCoreThread except arranges that at least one
 * thread is started even if corePoolSize is 0.
 * 判断当前线程池中工作线程的数量
 * 如果小于核心线程数,就添加一个核心线程
 * 如果大于核心线程数,就添加一个非核心线程
 * 这里是空任务的原因:就是为了开启一个线程,去执行任务队列中排队的任务
 */
void ensurePrestart() {
    
    
	int wc = workerCountOf(ctl.get());
	if (wc < corePoolSize)
		addWorker(null, true);
	else if (wc == 0)
		addWorker(null, false);
}

ensurePrestart:这个方法中,添加一个worker对象,在前面线程池源码中我有记录过,其实就是线程池的线程会被包装成worker对象,存到线程池中,这里添加一个null,就是为了让线程去执行任务队列中的任务

入队方法

在调用super.getQueue().add(task);的时候,就会调用相应的offer方法进行入队

/**
 * 入队方法,将待执行的任务插入到队列中
 * 在入队的时候,会进行优先级的判断
 * @param x
 * @return
 */
public boolean offer(Runnable x) {
    
    
	if (x == null)
		throw new NullPointerException();
	RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
	final ReentrantLock lock = this.lock;
	/**
	 * 1.加锁
	 */
	lock.lock();
	try {
    
    
		int i = size;
		/**
		 * 2.判断是否需要进行扩容
		 * 这里的扩容,和ArrayList扩容的方法类型:
		 * 先扩容50%,然后通过Arrays.copy将数组扩容之后的数据,再复制到queue中
		 */
		if (i >= queue.length)
			grow();
		size = i + 1;
		/**
		 * 3.如果当前插入的是第一个任务
		 * 就将e设置为头结点
		 *
		 * 否则的话,就进行优先级的处理
		 */
		if (i == 0) {
    
    
			queue[0] = e;
			setIndex(e, 0);
		} else {
    
    
			siftUp(i, e);
		}
		/**
		 * 4.这里是如果插入了第一个元素,去通知
		 * take方法,这里的available是一个condition对象
		 */
		if (queue[0] == e) {
    
    
			leader = null;
			available.signal();
		}
	} finally {
    
    
		lock.unlock();
	}
	return true;
}

上面入队的方法,注释写的还算可以,所以就不做过多说明,在入队的时候,有一个关键方法,就是对队列中的元素进行排队,其实就是在新插入一个元素的时候,要判断下当前元素要插入到哪里,根据元素对应的过期时间,也就是执行时间来比较,越早执行的,放到前面

优先级排序

/**
 * Sifts element added at bottom up to its heap-ordered spot.
 * Call only when holding lock.
 * 这是DelayedWorkQueue自己实现的,在入队时,进行优先级判断的逻辑
 * k:当前待插入元素要入队的位置
 * key:就是要入队的任务
 */
private void siftUp(int k, RunnableScheduledFuture<?> key) {
    
    
	while (k > 0) {
    
    
		/**
		 * 1.获取到k对应的父节点元素
		 */
		int parent = (k - 1) >>> 1;
		RunnableScheduledFuture<?> e = queue[parent];
		/**
		 * 2.如果任务k执行的时间晚于e父节点的,就无需再遍历处理
		 * 如果k的执行时间早于e,那就需要交换位置,然后再次遍历判断父节点和交换之后的优先级
		 */
		if (key.compareTo(e) >= 0)
			break;
		queue[k] = e;
		setIndex(e, k);
		k = parent;
	}
	/**
	 * 设置待插入元素的实际位置
	 */
	queue[k] = key;
	setIndex(key, k);
}

在比较的时候,是和当前元素的父元素比较,因为采用的是二叉树来存储,判断的核心方法就是compareTo方法,RunnableScheduledFuture也重新覆写了该方法

/**
 * 用来比较优先级,这里的other是插入元素要对比的元素
 * @param other
 * @return
 */
public int compareTo(Delayed other) {
    
    
	if (other == this) // compare zero if same object
		return 0;
	if (other instanceof ScheduledFutureTask) {
    
    
		ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
		/**
		 * 如果当前要插入的元素对应的时间 早于X节点执行,那就返回-1
		 * 如果要插入的元素对应的执行时间 晚于X节点执行,那就返回1
		 * 举例:X要在5S之后执行,但是当前插入的元素在2S之后执行,那这里的diff就小于0,返回-1
		 * 如果X要5S之后执行,但是待插入元素是10S之后执行,那这里的diff就大于0,返回1
		 * 至于下面的sequenceNumber应该是在任务是同时执行的情况下,再进行的优先级判断吧
		 */
		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;
	}
	/**
	 * 如果要比较的任务不是ScheduledFutureTask,那就直接获取到每个任务还有多少毫秒要执行,进行优先级判断
	 */
	long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
	return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}

执行队列中的任务

由于在入队的时候,我门指定的任务被包装成了ScheduledFutureTask,所以执行的时候,会执行对应的run方法

/**
 * Overrides FutureTask version so as to reset/requeue if periodic.
 * 自定义的任务在执行的时候,实际调用的就是这个方法,因为线程池对任务进行了一层包装
 */
public void run() {
    
    
	/**
	 * 1.首先判断是否需要重复执行,这个值是在初始化的时候,指定的
	 * 如果只需要执行一次,这里返回的就是false
	 * 如果需要周期定时执行,这里返回的就是true
	 * 根据period的值来判断
	 */
	boolean periodic = isPeriodic();
	/**
	 * 2.这里没看懂,判断是否需要取消任务?
	 */
	if (!canRunInCurrentRunState(periodic))
		cancel(false);
	/**
	 * 3.如果只需要执行一次,就会执行这里的逻辑
	 */
	else if (!periodic)
		ScheduledFutureTask.super.run();
	/**
	 * 4.如果是需要重复执行的,就执行这里的方法
	 * 如果任务正常执行成功,就继续设置下次的执行时间
	 * setNextRunTime():是在当前时间的基础之上,加上第一次指定的延迟时间
	 * reExecutePeriodic():是将任务再次加入队列中
	 */
	else if (ScheduledFutureTask.super.runAndReset()) {
    
    
		setNextRunTime();
		reExecutePeriodic(outerTask);
	}
}

第四点 就是可以重复执行的关键点,在判断需要重复执行的时候,就会再次入队
1、而判断是否可以重复执行,又是根据ScheduledFutureTask构造方法来决定的,在下面可以看scheduleAtFixedRate和schedule构造函数的区别
2、在需要重复执行的时候,就会把任务再次入队

/**
     * Requeues a periodic task unless current run state precludes it.
     * Same idea as delayedExecute except drops task rather than rejecting.
     *
     * 将需要重复执行的任务,再次入队
     * @param task the task
     */
    void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    
    
        if (canRunInCurrentRunState(true)) {
    
    
            super.getQueue().add(task);
            if (!canRunInCurrentRunState(true) && remove(task))
                task.cancel(false);
            else
                ensurePrestart();
        }
    }

构造方法

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

构造方法的话,就选取了其中一个来看,构造方法也是去初始化一个线程池,特殊的地方就在于,这个线程池类,不支持指定队列,只能使用DelayedWorkQueue,指定使用这个队列,是为了实现优先级排序

schedule

/**
 * @throws RejectedExecutionException {@inheritDoc}
 * @throws NullPointerException       {@inheritDoc}
 * 这是只执行一次的方法,也就是说:callable会在delay秒之后执行
 * 执行一次之后,就结束
 */
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;
}

这个方法就简单明了了,将指定的任务,在当前时间的基础之上,加上指定的延迟事件delay,放入到队列中,然后由线程池的线程去任务队列中获取执行

定时周期执行

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

/**
 * @throws RejectedExecutionException {@inheritDoc}
 * @throws NullPointerException       {@inheritDoc}
 * @throws IllegalArgumentException   {@inheritDoc}
 */
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;
}

这两个方法一起看下,只有一个地方不一样,就是在将任务包装成ScheduledFutureTask对象的时候,一个是负数,一个是正数,这里的区别还没有研究到,后面再说

总体这里的逻辑是:
1.将任务包装成ScheduledFutureTask
2.将任务赋值到一个临时变量中,这个临时变量outerTask 很重要,是再次入队的关键变量
3.然后即将任务入队

总结:

  1. ScheduledThreadPoolExecutor支持周期性定时任务,在jdk6之前是依赖于DelayQueue,但是在jdk6之后,ScheduledThreadPoolExecutor自己实现了类似DelayQueue的逻辑,就是DelayedWorkQueue
  2. 在入队的时候,会进行优先级的判断,所谓的优先级,就是根据任务执行的先后来排序,放在前面的,永远是最近要执行的
  3. 在出队之后,会判断任务是需要重复执行的,还是只执行一次
  4. 如果只需要执行一次,执行完就结束
  5. 如果需要执行多次,在执行完之后,会将任务再次入队,相同的任务,相同的延迟时间,入队会再进行一次优先级排队

猜你喜欢

转载自blog.csdn.net/CPLASF_/article/details/112093362