Java并发编程:Timer和TimerTask(转载)html
下面内容转载自:java
http://blog.csdn.net/xieyuooo/article/details/8607220编程
其实就Timer来说就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类,而具体的TimerTask须要由你本身来实现,例如这样:api
Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("abc"); } }, 200000 , 1000);
这里直接实现一个TimerTask(固然,你能够实现多个TimerTask,多个TimerTask能够被一个Timer会被分配到多个Timer中被调度,后面会说到Timer的实现机制就是说内部的调度机制),而后编写run方法,20s后开始执行,每秒执行一次,固然你经过一个timer对象来操做多个timerTask,其实timerTask自己没什么意义,只是和timer集合操做的一个对象,实现它就必然有对应的run方法,以被调用,他甚至于根本不须要实现Runnable,由于这样每每混淆视听了,为何呢?也是本文要说的重点。数组
在说到timer的原理时,咱们先看看Timer里面的一些常见方法:安全
public void schedule(TimerTask task, long delay)
这个方法是调度一个task,通过delay(ms)后开始进行调度,仅仅调度一次。数据结构
public void schedule(TimerTask task, Date time)
在指定的时间点time上调度一次。多线程
public void schedule(TimerTask task, long delay, long period)
这个方法是调度一个task,在delay(ms)后开始调度,每次调度完后,最少等待period(ms)后才开始调度。并发
public void schedule(TimerTask task, Date firstTime, long period)
和上一个方法相似,惟一的区别就是传入的第二个参数为第一次调度的时间。oop
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
调度一个task,在delay(ms)后开始调度,而后每通过period(ms)再次调度,貌似和方法:schedule是同样的,其实否则,后面你会根据源码看到,schedule在计算下一次执行的时间的时候,是经过当前时间(在任务执行前获得) + 时间片,而scheduleAtFixedRate方法是经过当前须要执行的时间(也就是计算出如今应该执行的时间)+ 时间片,前者是运行的实际时间,然后者是理论时间点,例如:schedule时间片是5s,那么理论上会在五、十、1五、20这些时间片被调度,可是若是因为某些CPU征用致使未被调度,假如等到第8s才被第一次调度,那么schedule方法计算出来的下一次时间应该是第13s而不是第10s,这样有可能下次就越到20s后而被少调度一次或屡次,而scheduleAtFixedRate方法就是每次理论计算出下一次须要调度的时间用以排序,若第8s被调度,那么计算出应该是第10s,因此它距离当前时间是2s,那么再调度队列排序中,会被优先调度,那么就尽可能减小漏掉调度的状况。
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
方法同上,惟一的区别就是第一次调度时间设置为一个Date时间,而不是当前时间的一个时间片,咱们在源码中会详细说明这些内容。
接下来看源码
首先看Timer的构造方法有几种:
构造方法1:无参构造方法,简单经过Tiemer为前缀构造一个线程名称:
public Timer() { this("Timer-" + serialNumber()); }
建立的线程不为主线程,则主线程结束后,timer自动结束,而无需使用cancel来完成对timer的结束。
构造方法2:传入了是否为后台线程,后台线程当且仅当进程结束时,自动注销掉。public Timer(boolean isDaemon) { this("Timer-" + serialNumber(), isDaemon); }
另外两个构造方法负责传入名称和将timer启动:
public Timer(String name, boolean isDaemon) { thread.setName(name); thread.setDaemon(isDaemon); thread.start(); }
这里有一个thread,这个thread很明显是一个线程,被包装在了Timer类中,咱们看下这个thread的定义是:
private TimerThread thread = new TimerThread(queue);
而定义TimerThread部分的是:
class TimerThread extends Thread {
看到这里知道了,Timer内部包装了一个线程,用来作独立于外部线程的调度,而TimerThread是一个default类型的,默认状况下是引用不到的,是被Timer本身所使用的。
接下来看下有那些属性
除了上面提到的thread,还有一个很重要的属性是:
private TaskQueue queue = new TaskQueue();
看名字就知道是一个队列,队列里面能够先猜猜看是什么,那么大概应该是我要调度的任务吧,先记录下了,接下来继续向下看:
里面还有一个属性是:threadReaper,它是Object类型,只是重写了finalize方法而已,是为了垃圾回收的时候,将相应的信息回收掉,作GC的回补,也就是当timer线程因为某种缘由死掉了,而未被cancel,里面的队列中的信息须要清空掉,不过咱们一般是不会考虑这个方法的,因此知道java写这个方法是干什么的就好了。
接下来看调度方法的实现:
对于上面6个调度方法,咱们不作一一列举,为何等下你就知道了:
来看下方法:
public void schedule(TimerTask task, long delay)
的源码以下:
public void schedule(TimerTask task, long delay) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); sched(task, System.currentTimeMillis()+delay, 0); }
这里调用了另外一个方法,将task传入,第一个参数传入System.currentTimeMillis()+delay可见为第一次须要执行的时间的时间点了(若是传入Date,就是对象.getTime()便可,因此传入Date的几个方法就不用多说了),而第三个参数传入了0,这里能够猜下要么是时间片,要么是次数啥的,不过等会就知道是什么了;另外关于方法:sched的内容咱们不着急去看他,先看下重载的方法中是如何作的
再看看方法:
public void schedule(TimerTask task, long delay,long 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); }
看来也调用了方法sched来完成调度,和上面的方法惟一的调度时候的区别是增长了传入的period,而第一个传入的是0,因此肯定这个参数为时间片,而不是次数,注意这个里的period加了一个负数,也就是取反,也就是咱们开始传入1000,在调用sched的时候会变成-1000,其实最终阅读完源码后你会发现这个算是老外对于一种数字的理解,而并不是有什么特殊的意义,因此阅读源码的时候也有这些困难所在。
最后再看个方法是:
public void scheduleAtFixedRate(TimerTasktask,long delay,long period)
源码为:
public void scheduleAtFixedRate(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); }
惟一的区别就是在period没有取反,其实你最终阅读完源码,上面的取反没有什么特殊的意义,老外不想增长一个参数来表示scheduleAtFixedRate,而scheduleAtFixedRate和schedule的大部分逻辑代码一致,所以用了参数的范围来做为区分方法,也就是当你传入的参数不是正数的时候,你调用schedule方法正好是获得scheduleAtFixedRate的功能,而调用scheduleAtFixedRate方法的时候获得的正好是schedule方法的功能,呵呵,这些讨论没什么意义,讨论实质和重点:
来看sched方法的实现体:
private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); synchronized(queue) { if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { 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(); } }
queue为一个队列,咱们先不看他数据结构,看到他在作这个操做的时候,发生了同步,因此在timer级别,这个是线程安全的,最后将task相关的参数赋值,主要包含nextExecutionTime(下一次执行时间),period(时间片),state(状态),而后将它放入queue队列中,作一次notify操做,为何要作notify操做呢?看了后面的代码你就知道了。
简言之,这里就是讲task放入队列queue的过程,此时,你可能对queue的结构有些兴趣,那么咱们先来看看queue属性的结构TaskQueue:
class TaskQueue { private TimerTask[] queue = new TimerTask[128]; private int size = 0;
可见,TaskQueue的结构很简单,为一个数组,加一个size,有点像ArrayList,是否是长度就128呢,固然不是,ArrayList能够扩容,它能够,只是会形成内存拷贝而已,因此一个Timer来说,只要内部的task个数不超过128是不会形成扩容的;内部提供了add(TimerTask)、size()、getMin()、get(int)、removeMin()、quickRemove(int)、rescheduleMin(long newTime)、isEmpty()、clear()、fixUp()、fixDown()、heapify();
这里面的方法大概意思是:
add(TimerTaskt)为增长一个任务
size()任务队列的长度
getMin()获取当前排序后最近须要执行的一个任务,下标为1,队列头部0是不作任何操做的。
get(inti)获取指定下标的数据,固然包括下标0.
removeMin()为删除当前最近执行的任务,也就是第一个元素,一般只调度一次的任务,在执行完后,调用此方法,就能够将TimerTask从队列中移除。
quickRmove(inti)删除指定的元素,通常来讲是不会调用这个方法的,这个方法只有在Timer发生purge的时候,而且当对应的TimerTask调用了cancel方法的时候,才会被调用这个方法,也就是取消某个TimerTask,而后就会从队列中移除(注意若是任务在执行中是,仍是仍然在执行中的,虽然在队列中被移除了),还有就是这个cancel方法并非Timer的cancel方法而是TimerTask,一个是调度器的,一个是单个任务的,最后注意,这个quickRmove完成后,是将队列最后一个元素补充到这个位置,因此此时会形成顺序不一致的问题,后面会有方法进行回补。
rescheduleMin(long newTime)是从新设置当前执行的任务的下一次执行时间,并在队列中将其重新排序到合适的位置,而调用的是后面说的fixDown方法。
对于fixUp和fixDown方法来说,前者是当新增一个task的时候,首先将元素放在队列的尾部,而后向前找是否有比本身还要晚执行的任务,若是有,就将两个任务的顺序进行交换一下。而fixDown正好相反,执行完第一个任务后,须要加上一个时间片获得下一次执行时间,从而须要将其顺序与后面的任务进行对比下。
其次能够看下fixDown的细节为:
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++; // j indexes smallest kid if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } }
这种方式并不是排序,而是找到一个合适的位置来交换,由于并非经过队列逐个找的,而是每次移动一个二进制为,例如传入1的时候,接下来就是二、四、八、16这些位置,找到合适的位置放下便可,顺序未必是彻底有序的,它只须要看到距离调度部分的越近的是有序性越强的时候就能够了,这样便可以保证必定的顺序性,达到较好的性能。
最后一个方法是heapify,其实就是将队列的后半截,所有作一次fixeDown的操做,这个操做主要是为了回补quickRemove方法,当大量的quickRmove后,顺序被打乱后,此时将一半的区域作一次很是简单的排序便可。
这些方法咱们不在说源码了,只须要知道它提供了相似于ArrayList的东西来管理,内部有不少排序之类的处理,咱们继续回到Timer,里面还有两个方法是:cancel()和方法purge()方法,其实就cancel方法来说,一个取消操做,在测试中你会发现,若是一旦执行了这个方法timer就会结束掉,看下源码是什么呢:
public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); queue.notify(); // In case queue was already empty. } }
貌似仅仅将队列清空掉,而后设置了newTasksMayBeScheduled状态为false,最后让队列也调用了下notify操做,可是没有任何地方让线程结束掉,那么就要回到咱们开始说的Timer中包含的thread为:TimerThread类了,在看这个类以前,再看下Timer中最后一个purge()类,当你对不少Task作了cancel操做后,此时经过调用purge方法实现对这些cancel掉的类空间的回收,上面已经提到,此时会形成顺序混乱,因此须要调用队里的heapify方法来完成顺序的重排,源码以下:
public int purge() { int result = 0; synchronized(queue) { for (int i = queue.size(); i > 0; i--) { if (queue.get(i).state == TimerTask.CANCELLED) { queue.quickRemove(i); result++; } } if (result != 0) queue.heapify(); } return result; }
那么调度呢,是如何调度的呢,那些notify,和清空队列是如何作到的呢?咱们就要看看TimerThread类了,内部有一个属性是:newTasksMayBeScheduled,也就是咱们开始所说起的那个参数在cancel的时候会被设置为false。
另外一个属性定义了
private TaskQueue queue;
也就是咱们所调用的queue了,这下联通了吧,不过这里是queue是经过构造方法传入的,传入后赋值用以操做,很明显是Timer传递给这个线程的,咱们知道它是一个线程,因此执行的中心天然是run方法了,因此看下run方法的body部分是:
public void run() { try { mainLoop(); } finally { synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } }
try很简单,就一个mainLoop,看名字知道是主循环程序,finally中也就是必然执行的程序为将参数为为false,并将队列清空掉。
那么最核心的就是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 // 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) { } } }
能够发现这个timer是一个死循环程序,除非遇到不能捕获的异常或break才会跳出,首先注意这段代码:
while (queue.isEmpty() &&newTasksMayBeScheduled) queue.wait();
循环体为循环过程当中,条件为queue为空且newTasksMayBeScheduled状态为true,能够看到这个状态其关键做用,也就是跳出循环的条件就是要么队列不为空,要么是newTasksMayBeScheduled状态设置为false才会跳出,而wait就是在等待其余地方对queue发生notify操做,从上面的代码中能够发现,当发生add、cancel以及在threadReaper调用finalize方法的时候会被调用,第三个咱们基本能够不考虑其实发生add的时候也就是当队列仍是空的时候,发生add使得队列不为空就跳出循环,而cancel是设置了状态,不然不会进入这个循环,那么看下面的代码:
if (queue.isEmpty()) break;
当跳出上面的循环后,若是是设置了newTasksMayBeScheduled状态为false跳出,也就是调用了cancel,那么queue就是空的,此时就直接跳出外部的死循环,因此cancel就是这样实现的,若是下面的任务还在跑还没运行到这里来,cancel是不起做用的。
接下来是获取一个当前系统时间和上次预计的执行时间,若是预计执行的时间小于当前系统时间,那么就须要执行,此时断定时间片是否为0,若是为0,则调用removeMin方法将其移除,不然将task经过rescheduleMin设置最新时间并排序:
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); } }
这里能够看到,period为负数的时候,就会被认为是按照按照当前系统时间+一个时间片来计算下一次时间,就是前面说的schedule和scheduleAtFixedRate的区别了,其实内部是经过正负数来断定的,也许java是不想增长参数,而又想增长程序的可读性,才这样作,其实经过正负断定是有些诡异的,也就是你若是在schedule方法传入负数达到的功能和scheduleAtFixedRate的功能是同样的,相反在scheduleAtFixedRate方法中传入负数功能和schedule方法是同样的。
同时你能够看到period为0,就是只执行一次,因此时间片正负0都用上了,呵呵,而后再看看mainLoop接下来的部分:
if (!taskFired)// Taskhasn't yet fired; wait queue.wait(executionTime- currentTime);
这里是若是任务执行时间还未到,就等待一段时间,固然这个等待极可能会被其余的线程操做add和cancel的时候被唤醒,由于内部有notify方法,因此这个时间并非彻底准确,在这里大多数状况下是考虑Timer内部的task信息是稳定的,cancel方法唤醒的话是另外一回事。
最后:
if (taskFired) // Task fired; run it, holding no locks task.run();
若是线程须要执行,那么调用它的run方法,而并不是启动一个新的线程或从线程池中获取一个线程来执行,因此TimerTask的run方法并非多线程的run方法,虽然实现了Runnable,可是仅仅是为了表示它是可执行的,并不表明它必须经过线程的方式来执行的。
回过头来再看看:
Timer和TimerTask的简单组合是多线程的嘛?不是,一个Timer内部包装了“一个Thread”和“一个Task”队列,这个队列按照必定的方式将任务排队处理,包含的线程在Timer的构造方法调用时被启动,这个Thread的run方法无限循环这个Task队列,若队列为空且没发生cancel操做,此时会一直等待,若是等待完成后,队列仍是为空,则认为发生了cancel从而跳出死循环,结束任务;循环中若是发现任务须要执行的时间小于系统时间,则须要执行,那么根据任务的时间片重新计算下次执行时间,若时间片为0表明只执行一次,则直接移除队列便可。
可是是否能实现多线程呢?能够,任何东西是不是多线程彻底看我的意愿,多个Timer天然就是多线程的,每一个Timer都有本身的线程处理逻辑,固然Timer从这里来看并非很适合不少任务在短期内的快速调度,至少不是很适合同一个timer上挂不少任务,在多线程的领域中咱们更可能是使用多线程中的:
Executors.newScheduledThreadPool
来完成对调度队列中的线程池的处理,内部经过new ScheduledThreadPoolExecutor来建立线程池的Executor的建立,固然也能够调用:
Executors.unconfigurableScheduledExecutorService
方法来建立一个DelegatedScheduledExecutorService其实这个类就是包装了下下scheduleExecutor,也就是这只是一个壳,英文理解就是被委派的意思,被托管的意思。
具体的使用例子能够参考这篇博文: