前言
本篇文章将对计划线程池ScheduledThreadPoolExecutor
的使用进行说明。
正文
一. 延时执行有返回值的任务
延时执行的意思就是,向线程池提交任务时,会指定一个时间delay,线程池会延迟delay的时间再执行任务。通过ScheduledThreadPoolExecutor
的schedule()
方法能够提交延时执行的任务,提交的任务只会执行一次。示例如下。
public class ScheduledThreadPoolExecutorTest {
@Test
public void ScheduledThreadPoolExecutor延时执行Callable() throws Exception {
// 创建计划线程池
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor
= new ScheduledThreadPoolExecutor(2);
// 创建Callable对象
Callable<Long> callable = new Callable<Long>() {
final long currentTimeMillis = System.currentTimeMillis();
@Override
public Long call() throws Exception {
return System.currentTimeMillis() - currentTimeMillis;
}
};
// 延时2秒执行Callable
// 这里的ScheduledFuture实际就是ScheduledFutureTask
ScheduledFuture<Long> scheduledFuture = scheduledThreadPoolExecutor
.schedule(callable, 2, TimeUnit.SECONDS);
// 通过ScheduledFuture拿到执行结果
Long result = scheduledFuture.get();
System.out.println(result);
}
}
运行测试程序,打印如下。
即任务执行的时间点与提交任务的时间点的间隔为2秒。同时由于提交的任务是Callable
,所以可以通过ScheduledFuture
来拿到延时执行任务的结果。
二. 延时执行无返回值的任务
通过ScheduledThreadPoolExecutor
的schedule()
方法,也能延时执行一个无返回值的Runnable
任务,不过通过ScheduledFuture
只能拿到一个空返回值。示例如下。
public class ScheduledThreadPoolExecutorTest {
@Test
public void ScheduledThreadPoolExecutor延时执行Runnable() throws Exception {
// 创建计划线程池
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor
= new ScheduledThreadPoolExecutor(2);
// 创建Runnable对象
Runnable runnable = new Runnable() {
final long currentTimeMillis = System.currentTimeMillis();
@Override
public void run() {
System.out.println(System.currentTimeMillis() - currentTimeMillis);
}
};
// 延时2秒执行Runnable
ScheduledFuture<?> scheduledFuture = scheduledThreadPoolExecutor
.schedule(runnable, 2, TimeUnit.SECONDS);
// 可以通过get()方法来阻塞主线程,直到任务执行完毕
Object object = scheduledFuture.get();
// Runnable任务无返回值,get()方法会拿到一个null
Assert.assertNull(object);
}
}
运行测试程序,打印如下。
三. 固定周期执行无返回值的任务
固定周期执行任务,就是指定一个时间,任务会以指定的时间来周期的触发执行。固定周期执行任务有两种情况:第一种是周期大于等于任务执行时间,第二种是周期小于任务执行时间。
先看一下周期大于等于任务执行时间的示例,如下所示。
public class ScheduledThreadPoolExecutorTest {
@Test
public void ScheduledThreadPoolExecutor固定周期执行Runnable_周期大于任务执行时间() throws Exception {
// 创建计划线程池
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor
= new ScheduledThreadPoolExecutor(2);
CountDownLatch countDownLatch = new CountDownLatch(10);
// 创建Runnable对象
Runnable runnable = new Runnable() {
final AtomicLong count = new AtomicLong(0L);
// currentTimeMillis表示每次任务触发的时间点
long currentTimeMillis = System.currentTimeMillis();
@Override
public void run() {
long nowMillis = System.currentTimeMillis();
// perid表示任务触发的时间间隔
long perid = nowMillis - currentTimeMillis;
currentTimeMillis = nowMillis;
System.out.println(count.getAndIncrement() + "次,间隔毫秒数:" + perid);
// 模拟任务执行需要花费1秒钟
LockSupport.parkNanos(1000 * 1000 * 1000);
if (count.get() >= 10) {
// 执行达到10次,关闭线程池
scheduledThreadPoolExecutor.shutdown();
}
countDownLatch.countDown();
}
};
// 延时2秒,并以2秒作为固定周期来定时执行Runnable
ScheduledFuture<?> scheduledFuture = scheduledThreadPoolExecutor
.scheduleAtFixedRate(runnable, 2, 2, TimeUnit.SECONDS);
countDownLatch.await();
}
}
上述示例中,任务每次执行花费1秒,然后周期是2秒,运行测试程序,打印如下。
可以看到任务的触发周期是2秒,符合预期。
再看一下周期小于任务执行时间的示例,如下所示。
public class ScheduledThreadPoolExecutorTest {
@Test
public void ScheduledThreadPoolExecutor固定周期执行Runnable_周期小于任务执行时间() throws Exception {
// 创建计划线程池
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor
= new ScheduledThreadPoolExecutor(2);
CountDownLatch countDownLatch = new CountDownLatch(10);
// 创建Runnable对象
Runnable runnable = new Runnable() {
final AtomicLong count = new AtomicLong(0L);
// currentTimeMillis表示每次任务触发的时间点
long currentTimeMillis = System.currentTimeMillis();
@Override
public void run() {
long nowMillis = System.currentTimeMillis();
// perid表示任务触发的时间间隔
long perid = nowMillis - currentTimeMillis;
currentTimeMillis = nowMillis;
System.out.println(count.getAndIncrement() + "次,间隔毫秒数:" + perid);
// 模拟任务执行需要花费3秒钟
LockSupport.parkNanos(1000 * 1000 * 1000 * 3L);
if (count.get() >= 10) {
// 执行达到10次,关闭线程池
scheduledThreadPoolExecutor.shutdown();
}
countDownLatch.countDown();
}
};
// 延时3秒,并以2秒作为固定周期来定时执行Runnable
ScheduledFuture<?> scheduledFuture = scheduledThreadPoolExecutor
.scheduleAtFixedRate(runnable, 3, 2, TimeUnit.SECONDS);
countDownLatch.await();
}
}
上述示例中,任务每次执行花费3秒,然后周期是2秒,运行测试程序,打印如下。
可以看到任务的触发周期是3秒,与预期不符。
造成上述现象的原因就是:周期执行的任务,下一次的执行时间点是在上一次任务执行完毕后再设置,并且下一次的执行时间点是上一次设置的执行时间点加上周期。下面以一张示意图进行说明。
结合示意图,说明如下。
周期大于等于任务执行时间
。例如在1秒时,任务执行完毕,此时设置任务的下一次执行时间点为time = 0 + 2 = 2
秒,那么在时间来到2秒时,又开始执行任务,在3秒时,任务执行完毕,此时设置任务的下一次执行时间点为time = 2 + 2 = 4
秒,以此类推,触发任务执行的周期为2秒;周期小于任务执行时间
。例如在3秒时,任务执行完毕,此时设置任务的下一次执行时间点为time = 0 + 2 = 2
秒,由于这个时间点已经小于当前时间点,所以任务会立即又被执行,在6秒时,任务执行完毕,此时设置任务的下一次执行时间点为time = 2 + 2 = 4
秒,以此类推,触发任务执行的周期为3秒。
四. 固定延时执行无返回值的任务
固定延时执行任务,就是指定一个延时时间delay,上一次任务执行完毕后,延时delay的时间,再执行下一次任务。示例如下。
public class ScheduledThreadPoolExecutorTest {
@Test
public void ScheduledThreadPoolExecutor固定延时执行Runnable() throws Exception {
// 创建计划线程池
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor
= new ScheduledThreadPoolExecutor(2);
CountDownLatch countDownLatch = new CountDownLatch(10);
// 创建Runnable对象
Runnable runnable = new Runnable() {
final AtomicLong count = new AtomicLong(0L);
// currentTimeMillis表示每次任务触发的时间点
long currentTimeMillis = System.currentTimeMillis();
@Override
public void run() {
long nowMillis = System.currentTimeMillis();
// perid表示任务触发的时间间隔
long perid = nowMillis - currentTimeMillis;
currentTimeMillis = nowMillis;
System.out.println(count.getAndIncrement() + "次,间隔毫秒数:" + perid);
// 模拟任务执行需要花费3秒钟
LockSupport.parkNanos(1000 * 1000 * 1000 * 3L);
if (count.get() >= 10) {
// 执行达到10次,关闭线程池
scheduledThreadPoolExecutor.shutdown();
}
countDownLatch.countDown();
}
};
// 延时2秒,并以2秒作为固定延时来定时执行Runnable
ScheduledFuture<?> scheduledFuture = scheduledThreadPoolExecutor
.scheduleWithFixedDelay(runnable, 2, 2, TimeUnit.SECONDS);
countDownLatch.await();
}
}
运行测试程序,结果如下。
可以看到,触发任务执行的周期是5秒,这是因为任务执行会花费3秒,执行结束后,会设置下一次任务执行的时间点,下一次执行的时间点是任务上一次执行结束的时间点加上延时,所以任务执行的周期是5秒。下面是一个示意图。
总结
关于ScheduledThreadPoolExecutor
的使用总结如下。
ScheduledThreadPoolExecutor
提供了schedule()
方法,可以延时一定时间执行任务;ScheduledThreadPoolExecutor
提供了scheduleAtFixedRate()
方法,可以让任务周期的触发,但是如果任务的执行时间大于指定的周期,那么任务的触发周期会变成任务的执行时间;ScheduledThreadPoolExecutor
提供了scheduleWithFixedDelay()
方法,可以让任务执行完毕后,延时一定时间再进行下一次任务执行。