目录
定时线程池基本介绍
用来处理延迟任务或者定时任务
- 与ThreadPoolExecutor不同的是里面没有非核心线程概念(ThreadPoolExecutor的有关知识可以点击此链接查看)
它接受ScheduledFutureTask类型的任务,是线程调度任务的最小单位,有三种提交任务的方式
-
schedule
-
scheduledAtFixedRate
-
scheduledWithFixedDelay
ScheduledFutureTask继承结构图
它采用DelayQueue存储等待的任务
-
DelayQueue内部封装了一个PriorityQueue,他会根据time的先后时间顺序,若time相同则根据sequenceNumber排序
-
DelayQueue也是一个无界队列
ScheduledThreadPoolExecutor介绍
一种ThreadPoolExecutor,可以另外安排命令在给定延迟后运行,或定期执行。当需要多个工作线程时,或者当需要ThreadPoolExecutor(该类扩展)的额外灵活性或功能时,该类比Timer更可取。
延迟任务在启用之前执行,但在启用后何时开始没有任何实时保证。按提交的先进先出(FIFO)顺序启用为完全相同的执行时间安排的任务。
当提交的任务在运行前被取消时,执行将被抑制。默认情况下,此类已取消的任务不会自动从工作队列中删除,直到其延迟消失。虽然这样可以进行进一步的检查和监视,但也可能会导致取消任务的无限保留。要避免这种情况,请将setRemoveOnCancelPolicy设置为true,这将导致在取消任务时立即从工作队列中删除任务。
通过scheduleAtFixedRate或scheduleWithFixedDelay调度的任务的连续执行不会重叠。虽然不同的执行可能由不同的线程执行,但先前执行的效果先于后续执行的效果。
虽然该类继承自ThreadPoolExecutor,但一些继承的优化方法对它没有用处。特别是,因为它使用corePoolSize线程和无界队列充当固定大小的池,所以对maximumPoolSize的调整没有任何有用的效果。此外,将corePoolSize设置为零或使用allowCoreThreadTimeOut几乎从来都不是一个好主意,因为一旦有资格运行任务,这可能会使池中没有线程来处理任务。
扩展说明:此类重写execute和submit方法以生成内部ScheduledFuture对象,以控制每个任务的延迟和调度。为了保留功能,子类中这些方法的任何进一步重写都必须调用超类版本,这将有效地禁用额外的任务自定义。但是,此类提供了替代的受保护扩展方法decorateTask(可运行和可调用各一个版本),可用于自定义用于执行通过execute、submit、scheduleAtFixedRate和scheduleWithFixedDelay输入的命令的具体任务类型。默认情况下,ScheduledThreadPoolExecutor使用扩展FutureTask的任务类型。
ScheduledExecutorService介绍
/*
* 一种ExecutorService,可以安排命令在给定延迟后运行,或定期执行。
* schedule方法创建具有各种延迟的任务,并返回可用于取消或检查执行的任务对象。
* scheduleAtFixedRate和scheduleWithFixedDelay方法创建并执行定期运行直到取消的任务。
* 使用执行器提交的命令。execute(Runnable)和ExecutorService提交方法的调度请求延迟为
* 零。在调度方法中也允许零延迟和负延迟(但不允许周期),并将其视为立即执行的请求。
* 所有调度方法都接受相对延迟和周期作为参数,而不是绝对时间或日期。转换表示为java的绝对
* 时间是一件简单的事情。util。按要求的格式填写日期。例如,要在未来某个日期进行计划,可
* 以使用:schedule(task,date.getTime()-System.currentTimeMillis(),
* TimeUnit.millises)。但是,请注意,由于网络时间同步协议、时钟漂移或其他因素,相对延迟
* 的到期不必与启用任务的当前日期一致。
* Executors类为该包中提供的ScheduledExecutorService实现提供了方便的工厂方法。
*/
public interface ScheduledExecutorService extends ExecutorService {
/**
* 创建并执行在给定延迟后启用的ScheduledFuture
* @param command–要执行的任务
* @param delay–延迟的执行的时间
* @param unit–delay参数的时间单位
* @return 一个ScheduledFuture,表示任务的挂起完成,其{@code get()}方法将在完成时
* 返回{@code null}
* @throws RejectedExecutionException–如果无法安排任务执行
* @throws NullPointerException–如果命令为null
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 创建并执行在给定延迟后启用的ScheduledFuture
* @param command–要执行的任务
* @param delay–延迟的执行的时间
* @param unit–delay参数的时间单位
* @return 可用于提取结果或取消的ScheduledFuture
* @throws RejectedExecutionException–如果无法安排任务执行
* @throws NullPointerException–如果命令为null
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
/**
* 创建并执行一个周期性动作,该动作在给定的初始延迟后首先启用,然后在给定的时间段内
* 启用;也就是说,执行将在initialDelay之后开始,然后是initialDelay+period,然后是
* initialDelay+2*period,依此类推。如果任务的任何执行遇到异常,则会抑制后续执行。否
* 则,任务将仅通过取消或终止执行者而终止。如果此任务的任何执行时间超过其周期,则后
* 续执行可能会延迟开始,但不会同时执行。 向注册中心发送心跳
* @param command–要执行的任务
* @param initialDelay–延迟第一次执行的时间
* @param period:连续执行之间的时间
* @param unit–initialDelay和delay参数的时间单位
* @return 一个ScheduledFuture,表示任务的挂起完成,其get()方法将在取消时引发异常
* @throws RejectedExecutionException–如果无法安排任务执行
* @throws NullPointerException–如果命令为null
* @throws IllegalArgumentException–如果延迟小于或等于零
* 如果采用这种发送方式,假设period为2s,中间的逻辑为5s,initialDelay为1s
* 先延迟1s,执行中间逻辑5s,因为5s>2s,所以第二次的执行和第一次的执行中间不会有停顿时
* 间
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 创建并执行一个周期性操作,该操作在给定的初始延迟后首先启用,然后在一个执行的终止
* 和下一个执行的开始之间具有给定的延迟。如果任务的任何执行遇到异常,则会抑制后续执
* 行。否则,任务将仅通过取消或终止执行者而终止。
* @param command–要执行的任务
* @param initialDelay–延迟第一次执行的时间
* @param delay:次执行终止与下一次执行开始之间的延迟 该任务结束完之后在延迟delay秒
* @param unit–initialDelay和delay参数的时间单位
* @return 一个ScheduledFuture,表示任务的挂起完成,其get()方法将在取消时引发异常
* @throws RejectedExecutionException–如果无法安排任务执行
* @throws NullPointerException–如果命令为null
* @throws IllegalArgumentException–如果延迟小于或等于零
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
适用场景:
分布式锁-redis(看门狗)
SpringCloud-Eureka的结构图
利用scheduleAtFixedRate()方法模拟发送心跳:
//实体类
public class HeartBeat implements Serializable {
private String ip;
private String port;
private String appName;
public HeartBeat() {
}
public HeartBeat(String ip, String port, String appName) {
this.ip = ip;
this.port = port;
this.appName = appName;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
}
//服务端代码
@Slf4j
public class EurekaServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
while (true){
Socket accept = serverSocket.accept();
new Thread( () -> {
try {
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
byte[] arr = new byte[1024];
int readLength ;
StringBuilder sb = new StringBuilder();
while ((readLength = bis.read(arr))!= -1){
sb.append(new String(arr,0,readLength));
}
HeartBeat heartBeat = JSON.parseObject(sb.toString(), HeartBeat.class);
log.info("heartbeat info =>" + heartBeat);
}catch (Exception e){
e.printStackTrace();
}
}).start();
}
}
}
//客户端代码
@Slf4j
public class EurekaClient {
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPool =
new ScheduledThreadPoolExecutor(1);
scheduledThreadPool.scheduleAtFixedRate(() -> {
Socket socket = null;
try {
socket = new Socket("127.0.0.1", 8888);
BufferedOutputStream dos = new BufferedOutputStream(socket.getOutputStream());
HeartBeat heartBeat = new HeartBeat();
heartBeat.setAppName("Order-Service");
heartBeat.setIp("192.168.124.111");
heartBeat.setPort("1234");
String string = JSON.toJSONString(heartBeat);
log.info("發送的數據"+string);
dos.write(string.getBytes());
//使用缓冲流写数据时,一定要执行flush方法
// (卡在这好久,换了DataoutputStream封装socket.getoutputStream发现可以,才想到这个问题)
dos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
},1000,5000, TimeUnit.MILLISECONDS);
}
}
阻塞队列使用的是DelayedWorkQueue()
public class ScheduledThreadPoolRunner {
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPool = new ScheduledThreadPoolExecutor(1);
scheduledThreadPool.schedule(() ->{
System.out.println("我要延迟5s");
},5000,TimeUnit.MILLISECONDS);
scheduledThreadPool.schedule(() ->{
System.out.println("我要延迟10s");
},10000,TimeUnit.MILLISECONDS);
scheduledThreadPool.schedule(() ->{
System.out.println("我要延迟3s");
},3000,TimeUnit.MILLISECONDS);
scheduledThreadPool.schedule(() ->{
System.out.println("我要延迟1s");
},1000,TimeUnit.MILLISECONDS);
}
}
//打印结果:我要延迟1s,我要延迟3s,我要延迟5s,我要延迟10s
- 关于打印结果有此疑问>>>>如何进行排序的?? ---
- 通过堆结构实现的(在数据结构章节中会有相关知识,等补充完,会直接附上链接)
DelayedWorkQueue基于基于堆(堆是表现出来的一种逻辑结构&&存储结构是数组)的数据结构,与DelayQueue和PriorityQueue中的数据结构类似,只是每个ScheduledFutureTask也将其索引记录到堆数组中。这消除了在取消时查找任务的需要,大大加快了删除速度(从O(n)下降到O(logn)),并减少了垃圾保留,否则在清除之前等待元素上升到顶部会发生垃圾保留。但是,由于队列也可能包含非ScheduledFutureTasks的可运行的ScheduledFutures,我们不能保证有这样的索引可用,在这种情况下,我们会退回到线性搜索。(我们预计大多数任务不会被修饰,更快的案例将更常见。)所有堆操作都必须记录索引更改——主要是在siftUp和siftDown中。删除后,任务的heapIndex设置为-1。请注意,ScheduledFutureTasks最多只能在队列中出现一次(对于其他类型的任务或工作队列,这不一定是真的),因此它们由heapIndex唯一标识。
它的内部封装了一个PriorityQueue(根据时间去做排序),他会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
DelayQueue也是一个无界队列体现在静态内部类:DelayedWorkQueue中的offer()方法里面
接收ScheduledFutureTask类型的任务,是线程池调度的最小单位,有三种提交任务的方式
-
schedule
-
scheduleAtFixRate
-
scheduleWithFixRate
源码分析
scheduledThreadPool.scheduleWithFixedDelay(
//task
() -> System.out.println(System.currentTimeMillis() + "send heart beats");
}, 1000, 2000, TimeUnit.MILLISECONDS);
//进入到scheduleWithFixedDelay方法里面
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类(无返回值)
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));
//返回test:该方法被protect修饰,做拓展用的方法 t就是sft
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
//ScheduledFutureTask内部类里面的一个属性.t就是sft指向自己的一个属性
//reExecutePeriodic重新排队的实际任务
sft.outerTask = t;
delayedExecute(t);
return t;
}
/**
* 修改或替换用于执行可运行的任务。此方法可用于重写用于管理内部任务的具体类。默认实
* 现只返回给定的任务。
*
* @param runnable 提交的runnable
* @param task 为执行runnable命令而创建的任务
* @param <V> 任务结果的类型
* @return a task that can execute the runnable
* @since 1.6
*/
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
return task;
}
/** reExecutePeriodic重新排队的实际任务 */
RunnableScheduledFuture<V> outerTask = this;
/**
* 延迟或周期性任务的主要执行方法。如果线程池关闭,则拒绝该任务。否则会将任务添加到
* 队列中,并在必要时启动一个线程来运行它。
*(我们不能预先启动线程来运行任务,因为任务(可能)还不应该运行。)
* 如果在添加任务时关闭了线程池,如果状态和关机后运行参数要求,则取消并删除它。
*
* @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();
}
}
/**
* 与prestartCoreThread相同:即使corePoolSize为0,也至少会启动一个线程。
*/
void ensurePrestart() {
//得到线程数
int wc = workerCountOf(ctl.get());
//如果小于核心线程数
if (wc < corePoolSize)
//增加一个worker
addWorker(null, true);
//线程数为0,增加一个worker
else if (wc == 0)
addWorker(null, false);
}
//ScheduledFutureTask:run()方法
public void run() {
//是否为周期性任务
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
//设置下一次的执行时间
setNextRunTime();
//实际拿到的还是自己原来的任务
reExecutePeriodic(outerTask);
}
}
/**
* 设置下一次运行定期任务的时间。
*/
private void setNextRunTime() {
long p = period;
//大于0:基于scheduleAtFixedRate()方法提交任务
if (p > 0)
time += p;
//小于0:基于scheduleWithFixedDelay()方法提交任务
else
time = triggerTime(-p);
}
long triggerTime(long delay) {
//当前时间+要延迟的时间
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
/**
*
* 除非当前运行状态排除了周期性任务,否则会重新对其进行排队。与delayedExecute的想法
* 相同,只是放弃任务而不是拒绝。
* @param task the task
*/
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
//如果给定当前运行状态和关机后运行参数,则返回true。上一个任务是否已经完成
if (canRunInCurrentRunState(true)) {
//添加到队列里面
super.getQueue().add(task);
//如果不能
if (!canRunInCurrentRunState(true) && remove(task))
//撤销
task.cancel(false);
else
//同上面的方法,会增加一个线程执行任务
ensurePrestart();
}
}
常用的定时任务区别
- Timer:单线程,线程挂了,不会在创建线程执行任务
- 定时线程池:线程挂了,在提交任务,线程池会创建新的线程执行任务
- xxl-job:定时任务+分布式调度
- quartz:单机的定时任务,功能强大