共计 8936 个字符,预计需要花费 23 分钟才能阅读完成。
- 一个 JDK Timer 的例子。
- JDK Timer 蕴含的次要对象。
- Timer 对象剖析。
- TimerTask 对象剖析。
- 任务调度:一次性定时工作。
- 任务调度:屡次执行的定时工作(固定工夫点或固定工夫距离)。
- JDK Timer 是单线程的吗?
- Thread 和 Runable 的区别。
- 优缺点。
一个 JDK Timer 的例子
我的项目中其实是常常须要定时工作的,JDK 就提供了一个定时工作的实现 Timer,然而因为 JDK Timer 不是很灵便(比方不能反对 cron 表达式的执行打算),所以我的项目中理论用的应该比拟少。
不过 JDK Timer 的应用非常简单。
第一步:创立 Timer 对象。
第二步:扩大 TimerTask 实现其 run 办法。
第三步:调用 Timer 对象的 schedule 办法创立执行打算。
@Slf4j | |
public class TimerTaskDemo {public void runTimerA(){Timer timer1=new Timer("Timer"); | |
long delay=10000L; | |
timer1.schedule(new TimerTask(){ | |
@Override | |
public void run(){log.info("This is timerTaskA:" + Thread.currentThread().getId()); | |
timer1.cancel();}},delay); | |
} | |
public static void main(String[] args) {TimerTaskDemo timerTaskDemo=new TimerTaskDemo(); | |
log.info("There we come:" + Thread.currentThread().getId()); | |
timerTaskDemo.runTimerA();} |
运行后果:主线程输入,10 秒后定时工作执行。
21:27:17.313 [main] INFO com.example.demo.task.TimerTaskDemo - There we come:1 | |
21:27:27.340 [Timer] INFO com.example.demo.task.TimerTaskDemo - This is timerTaskA:11 |
JDK Timer 蕴含的次要对象
JDK 提供了定时控制器 Timer,次要包含:
- Timer:定时控制器。
- TimerTask:定时控制器被触发当前要执行的工作,是一个实现了 Runable 的抽象类,利用须要扩大实现 TimerTask 从而执行咱们的定时工作。
- Schedule:执行打算,理论是 Timer 的一个办法,依照肯定的规定绑定 TimerTask 到 Timer。
- TaskQueue:工作队列,每一个 Timer 都蕴含一个工作队列保留工作,以便 Timer 一个个取出并执行工作。
Timer 对象剖析
JDK 定时工作的次要对象就是这个定时器,负责定时工作的创立及调度执行。
蕴含两个重要属性:
private final TaskQueue queue = new TaskQueue(); | |
private final TimerThread thread = new TimerThread(queue); |
一个是工作队列 TaskQueue,另外一个是定时器线程 TimerThread,两个对象都是 Timer 对象初始化的时候间接创立,定时器线程 TimerThread 持有工作队列。
Timer#TaskQueue
TaskQueue 是 Timer 的外部类,顾名思义,是工作队列。
工作队列以数组保留,初始化长度 128。
private TimerTask[] queue = new TimerTask[128];
通过 add 办法将工作退出工作队列,如果队列已满则裁减队列容量(2 倍),之后通过 fixUp 调整队列程序,确保队列尽可能依照执行工夫的先后顺序排列。
void add(TimerTask task) { | |
// Grow backing store if necessary | |
if (size + 1 == queue.length) | |
queue = Arrays.copyOf(queue, 2*queue.length); | |
queue[++size] = task; | |
fixUp(size); | |
} |
TaskQueue 的其余办法咱们前面在调用到的时候再做剖析。
Timer#TimerThread
TimerThread 也是 Timer 的外部类。
TimerThread 是一个线程类,扩大了 Thread 并笼罩了他的 run 办法。
class TimerThread extends Thread { | |
boolean newTasksMayBeScheduled = true; | |
private TaskQueue queue; | |
TimerThread(TaskQueue queue) {this.queue = queue;} | |
public void run() { | |
try {mainLoop(); | |
} finally { | |
// Someone killed this Thread, behave as if Timer cancelled | |
synchronized(queue) { | |
newTasksMayBeScheduled = false; | |
queue.clear(); // Eliminate obsolete references} | |
} | |
} |
对象实例的同时初始化了工作列表, 并初始化 newTasksMayBeScheduled 为 true。
run 办法调用了一个叫 mainLoop() 的办法,办法名通知咱们应该是一个主循环。
咱们晓得定时工作是启动一个新线程执行工作,他可能一直定时执行的起因就是新线程启动之后不退出,期待工作打算的调度。这个 mainLoop 应该就是线程启动之后的期待办法,始终期待任务调度,非必要不退出。
mainLoop 代码稍后剖析。
Timer 对象创立
从后面的例子咱们晓得,JDK Timer 定时工作首先要创立一个 Timer 对象,看一下 Timer 的实例化办法:
public Timer(String name) {thread.setName(name); | |
thread.start();} |
给 TimerThread 设置 name,之后间接启动 TimerThread。
Timer 对象创立的时候就间接启动的线程,咱们下面说过的 mainLoop 办法就开始运行了。然而这个时候 Timer 的工作队列还是空的,所以 mainLoop 应该是只能空转。
咱们先简略看一眼 mainLoop 办法,验证一下:
private void mainLoop() {while (true) { | |
try { | |
TimerTask task; | |
boolean taskFired; | |
synchronized(queue) { | |
// Wait for queue to become non-empty | |
while (queue.isEmpty() && newTasksMayBeScheduled) | |
queue.wait(); | |
if (queue.isEmpty()) | |
break; // Queue is empty and will forever remain; die | |
// 省略代码... |
队列是空的,并且 newTasksMayBeScheduled=true,所以调用 queue.wait() 办法挂起队列,期待被再次唤醒。
如果队列始终为空并且 newTasksMayBeScheduled 始终为 true,并且也不通过其余伎俩完结以后线程的话,他就会始终期待上来。
好了,是时候给工作队列喂点货色了。
TimerTask
顾名思义,TimerTask,就是工作、或者叫定时工作。
TimerTask 是一个实现了 Runable 接口的虚构类,他并没有实现 Runable 的 run 办法,须要咱们利用去实现。
TimerTask 才是咱们业务须要关注的次要指标,比方咱们须要每天晚上 2 点钟跑批进行账户余额的更新,那这个更新账户余额的业务办法就是要在业务对象(扩大 TimerTask)的 run 办法中去调用。
除了须要实现 run 办法去调用咱们的业务逻辑之外,还有两个属性理解一下:一个 lock,一个 state。咱们晓得 Timer 是线程平安的,lock 是用来在线程执行过程中更新工作状态的时候锁定工作的,state 是工作状态,包含:
- VIRGIN:工作尚未被调度。
- SCHEDULED:被调度然而尚未执行。
- EXECUTED:曾经执行实现。
- CANCELLED:被勾销。
任务调度:一次性工作
一次性工作指的是工作执行一次之后就完结,咱们下面的例子就是一个一次性工作。
一次性工作通过 Timer 的 schedule 办法调度:
Params: | |
task – task to be scheduled. | |
delay – delay in milliseconds before task is to be executed. | |
public void schedule(TimerTask task, long delay) |
schedule 办法接管两个参数:
- TimerTask:要执行的工作。
- delay:工作在 delay 毫秒之后触发
schedule 办法执行如下动作:
- 为确保 Timer 定时工作的线程平安行,同步 Timer 的工作队列。
- 同步 TimerTask 的 lock,并设置 TimerTask 的执行工夫为以后零碎工夫 +delay,设置工作状态为 SCHEDULED,设置工作的 period=0。
- TimerTask 退出工作队列。
- 从工作队列中获取待执行的工作(队首的工作),如果队首工作就是当前任务的话,调用工作队列 quene 的 notify() 办法唤醒队列。
咱们看到 schedule 只是将当前任务退出队列,退出队列之后工作什么工夫被执行就和 schedule 没有关系了,其实退出工作队列之后,schedule 就完成使命了。
工作具体什么时候被执行就是咱们下面所说的那个 mainLoop 的事件了,咱们后面说过,在 Timer 被创立之后,工作队列是空的,mainLoop 通过调用队列 queue.wait 处于无限期待命状态。
在此状态下利用通过 schedule 办法退出一个工作到工作队列中,并且以后队列如果只有刚被退出的这一个工作的话,就会调用 notify 唤醒队列。
咱们持续剖析 mainLoop 的残余代码,看一下工作队列被唤醒之后的逻辑。
private void mainLoop() {while (true) { | |
try { | |
TimerTask task; | |
boolean taskFired; | |
synchronized(queue) { | |
// Wait for queue to become non-empty | |
while (queue.isEmpty() && newTasksMayBeScheduled) | |
queue.wait(); | |
// 从这儿开始... | |
if (queue.isEmpty()) | |
break; // Queue is empty and will forever remain; die | |
// Queue nonempty; look at first evt and do the right thing | |
long currentTime, executionTime; | |
task = queue.getMin(); | |
synchronized(task.lock) {if (task.state == TimerTask.CANCELLED) {queue.removeMin(); | |
continue; // No action required, poll queue again | |
} | |
currentTime = System.currentTimeMillis(); | |
executionTime = task.nextExecutionTime; | |
if (taskFired = (executionTime<=currentTime)) {if (task.period == 0) { // Non-repeating, remove | |
queue.removeMin(); | |
task.state = TimerTask.EXECUTED; | |
} else { // Repeating task, reschedule | |
queue.rescheduleMin( | |
task.period<0 ? currentTime - task.period | |
: executionTime + task.period); | |
} | |
} | |
} | |
if (!taskFired) // Task hasn't yet fired; wait | |
queue.wait(executionTime - currentTime); | |
} | |
if (taskFired) // Task fired; run it, holding no locks | |
task.run();} catch(InterruptedException e) {}} | |
} |
代码其实比较简单:
- 如果 newTasksMayBeScheduled=false,其实就是接管到了当前任务执行器要完结执行的信号了,此时如果工作队列空了,就完结 mainLoop,就相当于当前任务执行器线程要完结了。
- 否则,从工作队列中获取队首工作。
- 工作上锁。
- 查看当前任务,如果曾经被勾销的话,将工作移除队列,啥也不干了(当前任务不须要被执行)。
- 比拟工作执行工夫 nextExecutionTime 如果小于以后零碎工夫的话,执行以下步骤:
5.1 设置 taskFired=true,查看 period= 0 则表明是一次性工作,将该工作移除队列,并设置工作状态为 EXECUTED。
5.2 否则,是周期性工作,执行 queue.rescheduleMin 对当前任务重排。 - 如果 taskFired=false,则表明还没有到当前任务的执行工夫,则限时挂起当前任务队列。
- 否则 taskFired=true 则调用 TimeTask 的 run 办法执行工作。
所以咱们当初明确一次性工作之所以执行一次后就不会被再次调度的起因是,工作执行后就被移出了工作队列。周期性工作能被屡次执行的起因是每次执行后都会对该工作在工作队列中的地位进行重排!
另外,咱们还须要搞清楚一个问题: 如何完结定时控制器?
这个问题其实咱们在剖析 mainLoop 的代码是曾经取得的一半答案:newTasksMayBeScheduled=false 并且工作队列为空。
答案的另一半就是要晓得如何满足上述条件?须要从 Timer 控制器提供的办法中寻找,Timer 提供了一个 cancel 办法,cancel 办法在设置 newTasksMayBeScheduled 为 false 并清空工作队列之后,立即调用工作队列的 notify 唤醒队列、完结 Timer 控制器。
public void cancel() {synchronized(queue) { | |
thread.newTasksMayBeScheduled = false; | |
queue.clear(); | |
queue.notify(); // In case queue was already empty.} | |
} |
任务调度:周期性工作
JDK Timer 反对两种类型的周期性工作,一种是 fixRate 周期性工作,通过定时控制器 Timer 的 scheduleAtFixedRate 办法调度,另外一种是非 fixRate 的,通过带有 period 的一般的 schedule 办法调度。
两者有什么区别呢?
搞清楚两者区别之前,咱们须要首先理解一个概念,就是定时控制器 Timer 在调度工作的时候是无奈保障严格依照调度打算执行工作的(不思考工作执行时长对调度周期的影响,比方咱们假如工作被调度后霎时就能够执行实现),什么意思呢?
比方咱们通过调度办法 schedule 安顿在以后工夫 10 秒后执行一个 period=10 秒的定时工作,比方以后工夫正好是 12:00 正,那么咱们的冀望是从 12 点 10 秒执行一次工作,后续每隔 10 秒执行一次,现实的执行工夫就是 10 秒 20 秒 30 秒 40 秒 50 秒 … 以此类推。
然而这个执行工夫其实 Timer 是没有方法保障的,因为线程挂起之后再次被唤醒是依赖于 CPU 的调度的,CPU 在 10 秒执行了一次工作之后,下次工作不肯定能在 20 秒被唤醒,有可能是 22 秒或者 23 秒的时候才会被唤醒。
假如 22 秒的时候工作被唤醒,Timer 在安顿执行下次工作打算的时候提供了两个选项:
- fixRate:如果是通过 scheduleAtFixedRate 办法进行调度的(此时调度器外部的 period>0), 下次工作安顿在 30 秒执行。
- 如果是一般的 schedule 办法调度的(此时调度器外部的 period<0),下次工作安顿在以后零碎工夫 +10 秒,也就是被安顿在第 32 秒执行。
所以两者的区别就高深莫测了。
另外,既然 Timer 外部是通过 period 大于或小于 0 来管制周期性工作的执行策略的,那咱们是不是能够在调用调度办法 schedule 的时候通过 period 来管制执行策略呢?答案当然是不能够,否则了解起来就会乱套了:
public void schedule(TimerTask task, long delay, long period) {if (delay < 0) | |
throw new IllegalArgumentException("Negative delay."); | |
if (period <= 0) | |
throw new IllegalArgumentException("Non-positive period."); | |
sched(task, System.currentTimeMillis()+delay, -period); | |
} |
Timer 的几个变形调度办法 schedule 都不容许 period 小于 0。
JDK Timer 是单线程?
JDK Timer 是单线程执行这种说法其实比拟含糊,须要加以解释,否则容易混同。
对于主线程来说,JDK Timer 的调度以及工作执行是在新启动的线程中执行的,调度和工作执行线程是和主线程独立的线程。所以从这个角度来看的话,说 JDK Timer 是单线程貌似不太正当。
然而 JDK Timer 的任务调度是在 TimerThread 线程中进行的,在 TimerTread 的 mainLoop 办法中查看到工作队列中的当前任务应该被调度的时候,通过 TimerTask 的 run 办法执行工作。 咱们晓得 TimerTask 尽管实现了 Runable 接口,然而在 TimerThread 线程中间接调用 TimerTask 的 run 办法执行工作、而不是将 TimerTask 再次封装在一个新的 Thread 中通过 Thread 的 start 办法执行工作,这样的话 TimerTaks 其实就是在 TimerThread 线程中执行,而并不会开启一个新的线程。
所以咱们的论断就是:JDK Timer 通过 TimerThread 线程调度工作,同时也是通过 TimerThread 线程执行工作,调度工作和执行工作是在同一个线程中实现的。从这个角度来讲,咱们能够说 JDK Timer 是单线程的。
因而,如果一个 TimerTask 的执行工夫太长,超过了周期性工作的 period 的话,工作的下次执行工夫将会受到影响!
Thread 和 Runable 的区别
以上探讨过程中其实波及到了一个 Thread 和 Runable 区别的问题,咱们明天也不是专门探讨这个问题的,但既然波及到了,就简略说一下。
这个问题尽管被大家宽泛探讨,也有可能是一个比拟广泛的 Java 基础知识的面试问题。然而,集体了解,这个问题基本就不应该成为一个问题,因为两者其实没有什么可比性。
Thread 简直能够认为是咱们启动线程的惟一抉择,只通过 Runable 而不借助 Thread 的话,咱们是没有方法启动一个新线程的。
Runable 其实只是一个简略的接口,定义了一个 run 办法。Thread 实现了 Runable 接口,而且 Thread 有一个定义为 Runable 的属性 target。 因而能够说 Thread 和 Runable 是有分割的。
咱们启动一个线程的惟一办法还是通过 Thread,咱们能够继承 Thread 并笼罩他的 run 办法,这个时候从利用的角度看,整个启动线程的过程就和 Runable 没有半毛钱的关系。
另外咱们还能够自定义一个业务类,实现 Runable 接口,而后 new 一个 Thread 对象并且把咱们自定义的业务类作为参数送给 Thread 对象的 targe。这种形式下从利用的角度看,启动线程的过程才和 Runable 有了关系。
如果咱们只是自定义了一个类实现了 Runable 接口,然而不通过 Thread 绑定这个自定义的类,不是通过 Thread 的 start 办法调用自定义类的 run 办法、而是间接调用 run 办法的话(这个过程就和 TimerThread 通过调用 TimerTask 的 run 办法执行工作有点相似)。那么咱们尽管实现了 Runable 接口,也调用了 run 办法,然而整个过程都和“启动新线程执行工作”没有半毛钱的关系。 这种状况下咱们尽管调用了 Runable 的 run 办法然而却并没有启动新线程,run 办法仍然还是在原来的线程下运行! 这种状况下的 Runable 接口就和其余一般接口没有任何区别了,他只是个定义了一个 run 办法的一般接口。
也看到过很多对于两者区别的探讨中提到了两种形式下对于变量是共享还是隔离拜访的说法,集体认为齐全是跑偏了。这类观点认为 Thread 形式实现的多线程是独占成员变量的、而通过 Runable 实现的多线程是共享成员变量的。看过了他们列举的例子,其实是因为 Thread 形式下是 new 了多个 Thread 对象所以成员变量当然隔离的,因为他们基本就别离属于不同的对象。而 Runable 形式下就只是 new 了一个 Runable 对象,而后 new 了多个 Thread、启动多个线程执行的时候是把这个惟一的 Runable 对象传递给了 Thread 的 target,不同线程持有的是雷同的 Runable 对象作为他们的 target,同一个对象的成员变量当然是共享的。
JDK Timer 的优缺点
长处只有一个,就是 JDK 自带,不须要引入内部包,应用比较简单。
毛病是应用简略,调度策略比拟繁多,不能反对 cron 表达式,貌似也不能反对“几点开始、执行 10 次”这样的需要,这类需要都须要应用层想方法管制。
如果工作执行时长大于 period 的话,会影响到调度工夫。
调度策略比较简单、不灵便,可能也就是导致 JDK Timer 不被广泛应用的起因。
上一篇 基于 Mybatis 的分页管制 – PageHelper 分页管制底层原理