Java中的Timer源码分析及缺陷
2016-02-26 14:54
447 查看
使用Timer执行定时任务很简单,一般这样子写:
以上代码创建了一个定时器和定时任务,大部分情况下它都能正常工作,延迟10ms后,每隔2s就打印一个hello world。
但其实Timer是有一些问题的,程序中如果不注意就可能出现问题,下面来自《Java并发编程实战》:
1.Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间长度大于其周期时间长度,那么就会导致这一次的任务还在执行,而下一个周期的任务已经需要开始执行了,当然在一个线程内这两个任务只能顺序执行,有两种情况:对于之前需要执行但还没有执行的任务,一是当前任务执行完马上执行那些任务(按顺序来),二是干脆把那些任务丢掉,不去执行它们。至于具体采取哪种做法,需要看是调用schedule还是scheduleAtFixedRate。
2.如果TimerTask抛出了一个未检出的异常,那么Timer线程就会被终止掉,之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
下面通过分析Timer的源码来解释为什么会有上面的问题。
Timer的实现原理很简单,概括的说就是:Timer有两个内部类,TaskQueue和TimerThread,TaskQueue其实就是一个最小堆(按TimerTask下一个任务执行时间点先后排序),它存放该Timer的所有TimerTask,而TimerThread就是Timer新开的检查兼执行线程,在run中用一个死循环不断检查是否有任务需要开始执行了,有就执行它(注意还是在这个线程执行)。
因此Timer实现的关键就是调度方法,也就是TimerThread的run方法:
具体逻辑在mainLoop方法中实现:
从第14行开始,这里取出那个最先需要执行的TimerTask,然后22行判断executionTime<=currentTime,其中executionTime就是该TimerTask下一个周期任务执行的时间点,currentTime为当前时间点,如果为true说明该任务需要执行了(注意可能是一个过时任务,应该在过去某个时间点开始执行,但由于某种原因还没有执行),接着第23行判断task.period == 0,Timer中period默认为0表示该TimerTask只会执行一次,不会周期性地不断执行,所以为true那么就移除掉该TimerTask,然后待会会执行该TimerTask一次。如果task.period不为0,那就分为小于0和大于0,如果调用的是schedule方法:
那么period就小于0,如果调用的是scheduleAtFixedRate方法:
那么period就大于0。
回到mainLoop方法中,当period<0时,当前TimerTask下一次开始执行任务的时间就会被设置为currentTime - task.period,可理解为定时任务被重置,从现在开始,period周期间隔(那么之前预想在这个间隔内存在的任务执行就没了)后执行第一次任务,这种情况就是Timer的任务可能丢失问题。当period>0,当前TimerTask下一次开始执行任务的时间就会被设置为executionTime + task.period,即下一次任务还是按原来的算,因此如果这时executionTime + task.period还先于currentTime,那么下一个任务就会马上执行,也就是Timer的任务快速调用问题。
以上分析解释了第一点,下面解释第二点。
从代码上可以看到在死循环中只catch了一个InterruptedException,也就是当前线程被中断,因此Timer的线程是可以执行一段时间,然后被操作系统挂到一边休息,然后又回来继续执行的。但如果抛出其它异常,那么整个循环就挂掉,当然外层的run方法也没有catch任何异常:
这时就会造成线程泄露,同时之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
补充:对于上面说到的Timer线程执行到一半被挂到一边去,这种情况与任务执行时间过长类似,如果调用schedule方法的话就有可能导致任务丢失。在Android中,有一种叫长连接的东西,它需要客户端发心跳包确保连接的存在,如果使用Timer实现定时发心跳包就可能会有问题,如果Timer线程在执行过程中被换出去了,那么调用schedule的就很有可能导致心跳包没有发出去,而调用scheduleAtFixedRate又可能会导致Timer线程没有占用CPU时心跳包没发出去,某一时刻又快速地发送好几个心跳包。因此在Android中一般使用AlarmManager实现心跳包的定时发送。
从JDK5开始引入了ThreadPoolExecutor,它可以用来实现定时任务,因此一般建议使用它来实现定时任务:
Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { System.out.println("hello world"); } }; timer.schedule(task, 10, 2000);
以上代码创建了一个定时器和定时任务,大部分情况下它都能正常工作,延迟10ms后,每隔2s就打印一个hello world。
但其实Timer是有一些问题的,程序中如果不注意就可能出现问题,下面来自《Java并发编程实战》:
1.Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间长度大于其周期时间长度,那么就会导致这一次的任务还在执行,而下一个周期的任务已经需要开始执行了,当然在一个线程内这两个任务只能顺序执行,有两种情况:对于之前需要执行但还没有执行的任务,一是当前任务执行完马上执行那些任务(按顺序来),二是干脆把那些任务丢掉,不去执行它们。至于具体采取哪种做法,需要看是调用schedule还是scheduleAtFixedRate。
2.如果TimerTask抛出了一个未检出的异常,那么Timer线程就会被终止掉,之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
下面通过分析Timer的源码来解释为什么会有上面的问题。
Timer的实现原理很简单,概括的说就是:Timer有两个内部类,TaskQueue和TimerThread,TaskQueue其实就是一个最小堆(按TimerTask下一个任务执行时间点先后排序),它存放该Timer的所有TimerTask,而TimerThread就是Timer新开的检查兼执行线程,在run中用一个死循环不断检查是否有任务需要开始执行了,有就执行它(注意还是在这个线程执行)。
因此Timer实现的关键就是调度方法,也就是TimerThread的run方法:
public void run() { try { mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } }
具体逻辑在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) { } } }
从第14行开始,这里取出那个最先需要执行的TimerTask,然后22行判断executionTime<=currentTime,其中executionTime就是该TimerTask下一个周期任务执行的时间点,currentTime为当前时间点,如果为true说明该任务需要执行了(注意可能是一个过时任务,应该在过去某个时间点开始执行,但由于某种原因还没有执行),接着第23行判断task.period == 0,Timer中period默认为0表示该TimerTask只会执行一次,不会周期性地不断执行,所以为true那么就移除掉该TimerTask,然后待会会执行该TimerTask一次。如果task.period不为0,那就分为小于0和大于0,如果调用的是schedule方法:
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); }
那么period就小于0,如果调用的是scheduleAtFixedRate方法:
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就大于0。
回到mainLoop方法中,当period<0时,当前TimerTask下一次开始执行任务的时间就会被设置为currentTime - task.period,可理解为定时任务被重置,从现在开始,period周期间隔(那么之前预想在这个间隔内存在的任务执行就没了)后执行第一次任务,这种情况就是Timer的任务可能丢失问题。当period>0,当前TimerTask下一次开始执行任务的时间就会被设置为executionTime + task.period,即下一次任务还是按原来的算,因此如果这时executionTime + task.period还先于currentTime,那么下一个任务就会马上执行,也就是Timer的任务快速调用问题。
以上分析解释了第一点,下面解释第二点。
从代码上可以看到在死循环中只catch了一个InterruptedException,也就是当前线程被中断,因此Timer的线程是可以执行一段时间,然后被操作系统挂到一边休息,然后又回来继续执行的。但如果抛出其它异常,那么整个循环就挂掉,当然外层的run方法也没有catch任何异常:
public void run() { try { mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } }
这时就会造成线程泄露,同时之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
补充:对于上面说到的Timer线程执行到一半被挂到一边去,这种情况与任务执行时间过长类似,如果调用schedule方法的话就有可能导致任务丢失。在Android中,有一种叫长连接的东西,它需要客户端发心跳包确保连接的存在,如果使用Timer实现定时发心跳包就可能会有问题,如果Timer线程在执行过程中被换出去了,那么调用schedule的就很有可能导致心跳包没有发出去,而调用scheduleAtFixedRate又可能会导致Timer线程没有占用CPU时心跳包没发出去,某一时刻又快速地发送好几个心跳包。因此在Android中一般使用AlarmManager实现心跳包的定时发送。
从JDK5开始引入了ThreadPoolExecutor,它可以用来实现定时任务,因此一般建议使用它来实现定时任务:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5); ses.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("hello world"); } }, 10, 2000, TimeUnit.MILLISECONDS);
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 从源码安装Mysql/Percona 5.5
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序