Timer定时任务源码
Timer描述
TimerQueue的数据结构
单线程只能顺序执行任务
串行的操作Queue
所谓追赶
附加一份完全的源码走读
Timer描述
JDK中提供的定时任务有两种实现一种是JDK3提供的 Timer 另一个是JDK5提供的 ScheduledExecutorService 本文介绍的是Timer.
与Timer形成组合关系的类有三个。TimerTask,TimerThread,TaskQueue TimerTask封装了执行任务的细节,TimerThread继承自Thread是一个线程,TaskQueue是多个任务的集合。
这四个类的关系为:一个Timer实例对应一个TaskQueue,对应一个TimerThread,对应多个TimerTask.TimerThread需要TimerQueue。
所以执行Timer是用一个线程从队列中取出任务,一个一个执行。
Timer是用户类,面向用户的提供了6个开启定时任务方法,关闭定时任务,以及清理定时任务的方法。6个开启定时任务的方法从执行周期分为单次执行和循环执行。从执行速率来
说分为追赶和不追赶两种。关闭定时任务则取消所有的任务,清理任务则是清除TimerTask自身取消的任务。
1,2是执行一次任务.延迟时间time(T)不能小于0.
3,4是执行多次任务.延迟时间time(T)不能小于0,其中4是把Date换成了毫秒值.间隔执行period(P)(不追赶)
5,6是执行多次任务.延迟时间time(T)不能小于0,其中6是把Date换成了毫秒值.间隔执行period(P)(追赶)
public void schedule(TimerTask task, long delay);1
public void schedule(TimerTask task, Date time);2
public void schedule(TimerTask task, long delay, long period);3
public void schedule(TimerTask task, Date firstTime, long period);4
public void scheduleAtFixedRate(TimerTask task, long delay, long period);5
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period);6
public void cancel();
public int purge();
TimerQueue的数据结构
从类名最直观的感觉它是一个队列,但它是用数组实现的一个二叉堆(小根堆)。二叉堆是一个二叉树,它拥有二叉树的特点根节点有左右两个叶子节点。小根堆是根节点比叶子节点小,大根堆是根节点比叶子节点大。
而用数组实现小根堆的规律是,设根节点为n则叶子节点为2n,2n+1。理论上任何的叶子节点都可能晋升为根节点。所以任何一个节点的叶子节点都是该节点位置的2n,2n+1.下图是一个大根堆,从这个图可以形象的体现2n,2n+1
add(TimerTask task)将任务添加到数组中,从数组的1号下标开始,这也是为了满足2n,2n+1,如果从0开始就不满足这个特性了。如果数组满了则使用拷贝数组的方式2倍扩容。从TimerThread的角度看如果从TaskQueue中取出任务并且执行,必须要找到最近需要
执行的TimerTask,任务执行完毕后也应当将TimerTask从TaskQueue中删除。getMin()返回数组中1号元素,意味着1号位置的元素永远是执行时间最近的TimerTask,removeMin()则将队列末尾的TimerTask覆盖到1号元素,将末尾位置的TimerTask设为null。所以怎
么整理数组中的TimerTask才能将最近执行的任务放到1号位置是最关键的。
fixUp()fixDown()两个方法使数组堆化。
fixUp方法从数组的最后一个元素开始寻找它的根节点,因为数组的最后一个元素只可能是叶子节点不会是根节点。叶子节点/2得到根节点的位置,对比根节点的执行时间与叶子节点的执行时间,哪个小哪个放在前边,小的
先执行,交换的操作是很典型的temp交换。如果根节点不需要和叶子节点交换则停止向前继续寻找。一旦两个节点交换了则意味着根节点变化了,那么身为根节点的自己本身也是别人的叶子节点,所以需要继续/2,继续对比,直到到达数组1号位置,这也保证了1号
位置永远是最小的节点。
fixDown方法从数组的1号位置开始*2,*2+1的寻找,这也对应了n,2n,2n+1的操作,如果有2n+1首先要确定2n和2n+1谁是最小的,因为只需要使用最小的叶子节点与根节点对比。所以才有了j++这个操作,无论j是2n还是2n+1最终都需要和根节点比较,如果触发temp交换
则表示叶子节点已经变化了,那么身为叶子节点的本身也可能是根节点,所以需要从这个节点开始再次寻找它的2n,2n+1,直到数组的末尾。
heapify方法则用来处理数组中的数据完全错乱的清空。首先他从数组的中间位置开始调用fixDown()。从中间位置开始寻找2n,2n+1。如果触发了temp交换则会继续向更深的叶子节点继续执行算法。当fixDown()结束后首先保证了中间节点以及它的子节点都是有顺序的。
然后再从中间节点向前推进继续fixDown()....直到推进到1号位置后进行最后一次fixDown()这一套操作下来就把一个完全乱掉的数组重新堆化。
fclass TaskQueue {
//用数组结构构建的一个队列.其本质是一个二叉堆(小根堆,二叉树)
//所有对queue的操作都会加锁,锁对象就是这个数组.
private TimerTask[] queue = new TimerTask[128];
//队列中实际存储的Task数量
private int size = 0;
//返回已有Tasks数量
int size() {
return size;
}
//将TimerTask添加到数组的末尾。然后fixUp重新排序。
void add(TimerTask task) {
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
queue[++size] = task;
fixUp(size);
}
//获取最近一个可执行的任务.
TimerTask getMin() {
return queue[1];
}
//删除最小的元素,1号元素是最小的。具体原因是二叉堆的数据结构决定的。
//将末尾的元素放到1号位置,然后将末尾的元素置为null。使用fixDown排序。
void removeMin() {
queue[1] = queue[size];
queue[size--] = null;
fixDown(1);
}
/*核心方法,add()后调用。传入的是当前队列中已有元素的个数。
首先。这个是一个二叉堆数据结构。二叉堆分为大根堆和小根堆。
当前使用的是小根堆,根节点是最小的。左右节点一定比根节点小。
但左右节点不分左大右小或左小右大。这是前提。
根节点和叶子节点的关系是 n 2n 2n+1.
这也就是为什么从数组的1号位置开始计算而不是0.
当前k是1,k是根节点。那么的子节点是2和3.所以如果k<1则不需要进入循环
因为只有一个根节点,无需排序。
接下来j=k>>1 相当于j=k/2;如果k是2则j是1,如果k是3则j也是1.也就是说无论
是奇数还偶数/2以后都能正确的找到它的根节点。例如10/2是5,11/2也是5.
根据n 2n 2n+1推算。5是根节点,10,11是叶子结点。
计算j以后无论j是左节点还是右节点都只需要和根节点做比较。如果是左节点也就是
偶数那么它一定是跟父节点排序过的,如果是右节点(奇数)因为左节点已经和根节
点对比过,右节点只需要和根节点对比。所以左节点和右节点无需比较。
nextExecutionTime是TimerTask下次执行的时间,如果j<=k则表示根节点下次执行时间
比叶子节点小或者等于则不需要交换,直接停止循环。
如果j>k则表示根节点下次执行时间是大于叶子节点则需要将叶子节点变成根节点。此时
根节点变化了,那这个根节点同样也是别的节点的叶子节点,此时需要往上推。例如10/2是5
5和10交换了位置。但是5本身是2的叶子节点,是4的兄弟节点。但是4此时一定是比2大。
所以5和4无需比较,但是5和2是需要重新比较。因为5是原来的10.以此类推,直到k=1结束循环。
*/
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
/*核心方法 removeMin()后调用。传入的是1
k是1.从removeMin()作为切入点,removeMin()本质是把队列最末尾的TimerTask放到了1号
位置。因为只有队列末尾的元素才不是根节点,而是彻底的叶子节点。但此时你把本该是叶子
节点的TimerTask放到了1号位的根节点,就一定要考虑1号位置的叶子节点2,3是否需要交换。
如果跟2或者3交换了,那2和3也是有叶子节点的,依然要依次的循环作比较。直到根节点确实
比叶子节点小。
k<<1相当于k*2.根据n 2n 2n+1的推算。1这个根节点的叶子节点是2,3。也就是说是2和3要和1
做比较,或许还需要交换。那么2和3都需要和1做操作吗?不需要,例如2比3大,那3是小的只需
3和1对比。如果3比2大那2是小的只需要2和1对比。所以才出现了,在不下标越界的前提下。
j和j+1先比较,如果j+1大则表示右节点比左节点小则j++意味着右节点和根节点1比较,左节点不参与。
接下来根节点k和j或者j+1比较如果不需要交换则直接break,因为没有交换,叶子结点的结构就会保留。
如果根节点k和j交换了,此时就需要从j这个叶子节点再次查询它自己的叶子节点,看看是否需要重排序。
*/
private void fixDown(int k) {
int j;
while ((j = k << 1) <= size && j > 0) {
if (j < size && queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
j++;
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
//quickRemove()之后调用的。
//针对于被取消的TimerTask的做法是将末尾的TimerTask放到取消TimerTask的位置上。
//这样是改变了队列的结构。需要重排序。
//调用这个方法后,数组已经是完全无序的了,仅仅是一个数组不是二叉堆。
//将数组堆化的方式,从数组的中间开始,调用fixDown。fixDown是把父节点和子节点对比。
//如果父节点比子节点大则交换位置,如果交换位置了,则在子节点的位置在重新和子节点
//的子节点做对比。直到不需要交换。heapify()从数组中间开始调用fixDown。举个例子。
//数组是10.第一次fixDown(5)。参与对比的是5,10.第二次fixDown(4)。参与对比的是8,9.
//第三次fixDown(3).参与对比的是6,7.第四次fixDown(2)。参与对比的是4,5.第五次fixDown(1)
//参与对比的是2,3.整个一圈下来就把数组变成了二叉堆。
void heapify() {
for (int i = size/2; i >= 1; i--)
fixDown(i);
}
}
单线程只能顺序执行任务
在Timer创建后就会启动TimerThread线程,TimerThread的run方法会执行定时任务的核心方法mainLoop()
mainLoop是一个while(true)循环,进入循环后如果判断队列中没有任务则会wait(),将线程等待。如果有任务则查看任务是否是取消状态,如果不是则继续执行,获取当前系统时间然后和队列中第一个任务的执行时间做对比,如果执行时间小于当前时间则表示
任务可以执行,如果任务时间还没到则继续wait(time),此时线程等待的时间是当前时间和任务执行时间差。如果任务已经可以执行则调用TimerTask的run方法。从这个执行顺序看,就算是多个任务都到了执行时间,那也会一个一个执行,因为只有一个线程。
如果当前执行的任务耗时特别长,mainLoop也只能等待当前TimerTask执行结束后才能循环下一个,所以很容易造成任务堆积。
TimerThread的方法。
private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) {//使用queue锁 //线程可用,并且队列内没有TimerTask,则进入等待。循环阻塞。 while (queue.isEmpty() && newTasksMayBeScheduled){ queue.wait(); } //防止队列为空,但线程不可用,此时要跳出循环。 if (queue.isEmpty()){ break; } long currentTime, executionTime; //获取最近一个可执行的任务. task = queue.getMin(); synchronized(task.lock) {//使用task锁 //如果最近的任务已经被取消则删除任务,循环继续。 if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; } //当前时间 currentTime = System.currentTimeMillis(); //TimerTask的下一次执行时间。 executionTime = task.nextExecutionTime; if (taskFired = (executionTime<=currentTime)) {//可执行 //如果period是0则表示这个TimerTask只执行一次。 if (task.period == 0) { //将TimerTask从队列中移除。 queue.removeMin(); //设置TimerTask的状态为执行中。 task.state = TimerTask.EXECUTED; } else { //period小于0或者大于0都表示要执行多次,需要重新给TimerTask设置下次执行时间。(<0不追赶)(>0追赶) //不追赶的TimerTask下次执行的时间是当前时间+间隔时间。 queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period); } } } //如果最近的一个任务还没有到执行时间,则进入等待,等待时间为执行时间和当前时间的差。 if (!taskFired){ queue.wait(executionTime - currentTime); } } //判断可执行,则调用TimerTask的run方法。执行。 if (taskFired) task.run(); } catch(InterruptedException e) { } } }
串行的操作Queue
上边提到如果队列中无任务,则线程等待,线程等待会释放queue的锁,此时Timer的sched()则可以执行添加任务。queue.add()将任务添加到TimerQueue中,并且触发fixUp方法从叶子节点开始整理二叉堆。当任务成功添加到队列后,queue.notify()唤醒在
mainLoop处的wait()。需要注意的是,只有操作Queue时才会触发锁,等待,唤醒操作。线程执行TimerTask的run()时锁已经释放。
Timer的方法
private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) {//使用queue锁 if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) {//使用task锁 if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); if (queue.getMin() == task) queue.notify();//第一个清除等待的位置 } }
所谓追赶
博客的开头说到6个开启任务的方法中分为追赶和不追赶。所谓追赶与不追赶在于如果firstTime是一个已经过去的时间。举一个场景,当前需要从今天下午三点开始执行任务,没过1分钟执行一次。到四点取消定时任务。当前的firstTime如果设置的是今天下午两点
对于不追赶的方法则会执行60次。而对于追赶的方法则会执行120次。这其中的奥妙在于period,如果是只执行一次的任务则period是0,如果是不追赶的方法传递的是-period,如果是追赶的则传递的是+period。在mainLoop()执行任务前会判断这个当前任务是否是
多次执行的。如果多次执行追赶的方法,则将下次执行的时间设置为当前时间+period,而多次执行不追赶的方法则将下次执行的时间设置为firstTime+period。也就是说,是否对于是否追赶取决于第一次执行任务时,设置的下一次任务执行时间的基准是以当前时间
为基准还是以firstTime为基准。
Timer的方法
public void schedule(TimerTask task, Date firstTime, long period) {//4 if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), -period); } public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period) {//6 if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), period); }
TimerThread的方法
private void mainLoop() { while(true){ ..... queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period); } }
TimerQueue的方法 void rescheduleMin(long newTime) { queue[1].nextExecutionTime = newTime; fixDown(1); }
附加一份完全的源码走读
public abstract class TimerTask implements Runnable { final Object lock = new Object();//只要是对任务做操作都会使用这把锁.每个TimerTask都有自己的锁. int state = VIRGIN; static final int VIRGIN = 0;//任务初始状态 static final int SCHEDULED = 1;//添加到队列后的状态 static final int EXECUTED = 2;//执行中 static final int CANCELLED = 3;//任务被取消 long nextExecutionTime;//下一次的执行时间 long period = 0;//间隔时间 protected TimerTask() { } public abstract void run(); //取消任务 public boolean cancel() { synchronized(lock) {//使用task锁 boolean result = (state == SCHEDULED);//只有被添加到队列的才可以取消(才算取消成功) state = CANCELLED; return result; } } public long scheduledExecutionTime() { synchronized(lock) {//使用task锁 return (period < 0 ? nextExecutionTime + period : nextExecutionTime - period); } } } public class Timer { private final TaskQueue queue = new TaskQueue();//一个定时器绑定一个任务队列 private final TimerThread thread = new TimerThread(queue);//一个定时器一个线程 //没用上,不知道干啥的 private final Object threadReaper = new Object() { protected void finalize() throws Throwable { synchronized(queue) {//使用queue锁 thread.newTasksMayBeScheduled = false; queue.notify();//第三个清除等待的位置 } } }; private final static AtomicInteger nextSerialNumber = new AtomicInteger(0); //拼接线程名称 private static int serialNumber() { return nextSerialNumber.getAndIncrement(); } public Timer() { this("Timer-" + serialNumber()); } //创建定时任务,给定名称,并启动. public Timer(String name) { thread.setName(name); thread.start(); } public Timer(boolean isDaemon) { this("Timer-" + serialNumber(), isDaemon); } //创建定时任务,给定名称,并启动.isDaemon=true则为守护线程,如果是守护线程则主线程执行结束后,守护线程也结束. public Timer(String name, boolean isDaemon) { thread.setName(name); thread.setDaemon(isDaemon); thread.start(); } ------------------------------------------------------------- public void schedule(TimerTask task, long delay) {//1 if (delay < 0) throw new IllegalArgumentException("Negative delay."); sched(task, System.currentTimeMillis()+delay, 0); } public void schedule(TimerTask task, Date time) {//2 sched(task, time.getTime(), 0); } public void schedule(TimerTask task, long delay, long period) {//3 if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, -period); } public void schedule(TimerTask task, Date firstTime, long period) {//4 if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), -period); } public void scheduleAtFixedRate(TimerTask task, long delay, long period) {//5 if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, period); } public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period) {//6 if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, firstTime.getTime(), period); } 1,2是执行一次任务.延迟时间time(T)不能小于0.间隔执行period(P)传入0 3,4是执行多次任务.延迟时间time(T)不能小于0,其中4是把Date换成了毫秒值.间隔执行period(P)传入的负数(不追赶) 5,6是执行多次任务.延迟时间time(T)不能小于0,其中6是把Date换成了毫秒值.间隔执行period(P)传入的正数(追赶) ------------------------------------------------------------- private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) {//使用queue锁 if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) {//使用task锁 if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); if (queue.getMin() == task) queue.notify();//第一个清除等待的位置 } } //终止此计时器,丢弃任何当前计划的任务。 public void cancel() { synchronized(queue) {//使用queue锁 thread.newTasksMayBeScheduled = false;//线程的成员变量标记线程是否可用.默认是true queue.clear();//清空队列 queue.notify();//第二个清除等待的位置 } } //从该计时器的任务队列中删除所有取消的任务。TimerTask.cancel()里将任务的状态修改为了CANCELLED public int purge() { int result = 0; synchronized(queue) {//使用queue锁 //循环将队列内所有已取消的任务清理掉. for (int i = queue.size(); i > 0; i--) { if (queue.get(i).state == TimerTask.CANCELLED) { queue.quickRemove(i);//删除Task result++;//记录删除数量. } } //有TimerTask是取消的。需要重新排序队列。 if (result != 0) queue.heapify(); } //返回清理成功的数量. return result; } } class TimerThread extends Thread { boolean newTasksMayBeScheduled = true;//如果为false表示线程不可用. private TaskQueue queue;//Timer的成员变量.里边众多Task TimerThread(TaskQueue queue) {//把队列给线程传递过去 this.queue = queue; } //Timer实例化后便会启动线程执行run(),意味着就算没有TimerTask也会执行线程. public void run() { try { mainLoop();//线程执行任务. } finally {//mainLoop循环结束 synchronized(queue) {//使用queue锁 newTasksMayBeScheduled = false; queue.clear(); //清空队列中的任务. } } } private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) {//使用queue锁 //线程可用,并且队列内没有TimerTask,则进入等待。循环阻塞。 while (queue.isEmpty() && newTasksMayBeScheduled){ queue.wait(); } //防止队列为空,但线程不可用,此时要跳出循环。 if (queue.isEmpty()){ break; } long currentTime, executionTime; //获取最近一个可执行的任务. task = queue.getMin(); synchronized(task.lock) {//使用task锁 //如果最近的任务已经被取消则删除任务,循环继续。 if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; } //当前时间 currentTime = System.currentTimeMillis(); //TimerTask的下一次执行时间。 executionTime = task.nextExecutionTime; if (taskFired = (executionTime<=currentTime)) {//可执行 //如果period是0则表示这个TimerTask只执行一次。 if (task.period == 0) { //将TimerTask从队列中移除。 queue.removeMin(); //设置TimerTask的状态为执行中。 task.state = TimerTask.EXECUTED; } else { //period小于0或者大于0都表示要执行多次,需要重新给TimerTask设置下次执行时间。(<0不追赶)(>0追赶) //不追赶的TimerTask下次执行的时间是当前时间+间隔时间。 queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period); } } } //如果最近的一个任务还没有到执行时间,则进入等待,等待时间为执行时间和当前时间的差。 if (!taskFired){ queue.wait(executionTime - currentTime); } } //判断可执行,则调用TimerTask的run方法。执行。 if (taskFired) task.run(); } catch(InterruptedException e) { } } } } class TaskQueue { //用数组结构构建的一个队列.其本质是一个二叉堆(小根堆,二叉树) //所有对queue的操作都会加锁,锁对象就是这个数组. private TimerTask[] queue = new TimerTask[128]; //队列中实际存储的Task数量 private int size = 0; //返回已有Tasks数量 int size() { return size; } //将TimerTask添加到数组的末尾。然后fixUp重新排序。 void add(TimerTask task) { if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); queue[++size] = task; fixUp(size); } //获取最近一个可执行的任务. TimerTask getMin() { return queue[1]; } //在清理队列时使用这个方法,仅查询. TimerTask get(int i) { return queue[i]; } //删除最小的元素,1号元素是最小的。具体原因是二叉堆的数据结构决定的。 //将末尾的元素放到1号位置,然后将末尾的元素置为null。使用fixDown排序。 void removeMin() { queue[1] = queue[size]; queue[size--] = null; fixDown(1); } //删除队列内指定位置的Task void quickRemove(int i) { assert i <= size;//断言删除的下标是否正确.在编译器内使用assert需要在VM里增加参数 -ea //队列尾部元素移动至删除的元素下标 queue[i] = queue[size]; //将队列尾部元素置位null queue[size--] = null; } //TimerTask重新分配下次执行时间。分配时间后需要fixDown重排序。 void rescheduleMin(long newTime) { queue[1].nextExecutionTime = newTime; fixDown(1); } //查询队列内是否有Task boolean isEmpty() { return size==0; } //删除队列内的所有Task void clear() { for (int i=1; i<=size; i++) //循环将Task置为null queue[i] = null; //队列内Task数量清0 size = 0; } /*核心方法,add()后调用。传入的是当前队列中已有元素的个数。 首先。这个是一个二叉堆数据结构。二叉堆分为大根堆和小根堆。 当前使用的是小根堆,根节点是最小的。左右节点一定比根节点小。 但左右节点不分左大右小或左小右大。这是前提。 根节点和叶子节点的关系是 n 2n 2n+1. 这也就是为什么从数组的1号位置开始计算而不是0. 当前k是1,k是根节点。那么的子节点是2和3.所以如果k<1则不需要进入循环 因为只有一个根节点,无需排序。 接下来j=k>>1 相当于j=k/2;如果k是2则j是1,如果k是3则j也是1.也就是说无论 是奇数还偶数/2以后都能正确的找到它的根节点。例如10/2是5,11/2也是5. 根据n 2n 2n+1推算。5是根节点,10,11是叶子结点。 计算j以后无论j是左节点还是右节点都只需要和根节点做比较。如果是左节点也就是 偶数那么它一定是跟父节点排序过的,如果是右节点(奇数)因为左节点已经和根节 点对比过,右节点只需要和根节点对比。所以左节点和右节点无需比较。 nextExecutionTime是TimerTask下次执行的时间,如果j<=k则表示根节点下次执行时间 比叶子节点小或者等于则不需要交换,直接停止循环。 如果j>k则表示根节点下次执行时间是大于叶子节点则需要将叶子节点变成根节点。此时 根节点变化了,那这个根节点同样也是别的节点的叶子节点,此时需要往上推。例如10/2是5 5和10交换了位置。但是5本身是2的叶子节点,是4的兄弟节点。但是4此时一定是比2大。 所以5和4无需比较,但是5和2是需要重新比较。因为5是原来的10.以此类推,直到k=1结束循环。 */ private void fixUp(int k) { while (k > 1) { int j = k >> 1; if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } } /*核心方法 removeMin()后调用。传入的是1 k是1.从removeMin()作为切入点,removeMin()本质是把队列最末尾的TimerTask放到了1号 位置。因为只有队列末尾的元素才不是根节点,而是彻底的叶子节点。但此时你把本该是叶子 节点的TimerTask放到了1号位的根节点,就一定要考虑1号位置的叶子节点2,3是否需要交换。 如果跟2或者3交换了,那2和3也是有叶子节点的,依然要依次的循环作比较。直到根节点确实 比叶子节点小。 k<<1相当于k*2.根据n 2n 2n+1的推算。1这个根节点的叶子节点是2,3。也就是说是2和3要和1 做比较,或许还需要交换。那么2和3都需要和1做操作吗?不需要,例如2比3大,那3是小的只需 3和1对比。如果3比2大那2是小的只需要2和1对比。所以才出现了,在不下标越界的前提下。 j和j+1先比较,如果j+1大则表示右节点比左节点小则j++意味着右节点和根节点1比较,左节点不参与。 接下来根节点k和j或者j+1比较如果不需要交换则直接break,因为没有交换,叶子结点的结构就会保留。 如果根节点k和j交换了,此时就需要从j这个叶子节点再次查询它自己的叶子节点,看看是否需要重排序。 */ private void fixDown(int k) { int j; while ((j = k << 1) <= size && j > 0) { if (j < size && queue[j].nextExecutionTime > queue[j+1].nextExecutionTime) j++; if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } } //quickRemove()之后调用的。 //针对于被取消的TimerTask的做法是将末尾的TimerTask放到取消TimerTask的位置上。 //这样是改变了队列的结构。需要重排序。 //调用这个方法后,数组已经是完全无序的了,仅仅是一个数组不是二叉堆。 //将数组堆化的方式,从数组的中间开始,调用fixDown。fixDown是把父节点和子节点对比。 //如果父节点比子节点大则交换位置,如果交换位置了,则在子节点的位置在重新和子节点 //的子节点做对比。直到不需要交换。heapify()从数组中间开始调用fixDown。举个例子。 //数组是10.第一次fixDown(5)。参与对比的是5,10.第二次fixDown(4)。参与对比的是8,9. //第三次fixDown(3).参与对比的是6,7.第四次fixDown(2)。参与对比的是4,5.第五次fixDown(1) //参与对比的是2,3.整个一圈下来就把数组变成了二叉堆。 void heapify() { for (int i = size/2; i >= 1; i--) fixDown(i); } }