上一节通过一个小例子分析了Timer运行过程,牵涉的执行线程虽然只有两个,但实际场景会比上面复杂一些。
首先通过一张简单类图(只列出简单的依赖关系)看一下Timer暴露的接口。
为了演示Timer所暴露的接口,下面举一个极端的例子(每一个接口方法面向单独的执行线程),照样以闹钟为例(源码只列出关键部分,下同)。
public class ScheduleDemo { public static void main(String[] args) throws Exception { AlarmTask alarm1 = new AlarmTask("闹钟1"); AlarmTask alarm2 = new AlarmTask("闹钟2"); new Thread("线程1"){ public void run() { log.info("["+Thread.currentThread().getName()+"]调度闹钟1"); timer.schedule(alarm1,delay,period); } }.start(); new Thread("线程2"){ public void run() { log.info("["+Thread.currentThread().getName()+"]调度闹钟2"); timer.schedule(alarm2,delay,period); } }.start(); Thread.sleep(1500); new Thread("线程3"){ public void run() { log.info("["+Thread.currentThread().getName()+"]取消闹钟2"); alarm2.cancel(); } }.start(); new Thread("线程4"){ public void run() { log.info("["+Thread.currentThread().getName()+"]清理无用闹钟"); timer.purge(); } }.start(); new Thread("线程5"){ public void run() { log.info("["+Thread.currentThread().getName()+"]关闭所有闹钟"); timer.cancel(); } }.start(); } /** * 模拟闹钟 */ static class AlarmTask extends TimerTask{ String name ; public AlarmTask(String name){ this.name=name; } public void run() { log.info("["+Thread.currentThread().getName()+"]-["+name+"]嘀。。。"); Thread.sleep(1000); //模拟闹钟执行时间 } }}
执行结果
[线程2]调度闹钟2[线程1]调度闹钟1[Timer-0]-[闹钟2]嘀。。。[线程3]取消闹钟2[线程4]清理无用闹钟[线程5]关闭所有闹钟
下面我们依次查看一下每个接口方法的源码。
1. 查看Timer.sched()源码
public void schedule(TimerTask task, long delay, long period) { sched(task, System.currentTimeMillis()+delay, -period);}private void sched(TimerTask task, long time, long period) { // 如果period无限大,保证其在一个合理的范围内 if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; // 加queue锁,保证队列操作的线程安全 synchronized(queue) { // 加lock锁,保证任务状态的一致性(多线程环境下) synchronized(task.lock) { task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } // 将任务加入队列实现排序 queue.add(task); if (queue.getMin() == task) queue.notify(); }}
其中queue.add(task在)将任务加入队列的同时实现了内部排序。
void add(TimerTask task) { // 队列不足时,以两倍容量扩增 if (size + 1 == queue.length) // 从性能上要快于new一个数组的效率 queue = Arrays.copyOf(queue, 2 * queue.length); queue[++size] = task; // 利用二分查找算法实现任务排序 fixUp(size);}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; }}
从方法sched()可以看到,该方法一方面持有queue锁,用来维护队列排序的线程安全;一方面持有lock锁,用来维护任务状态的线程安全。
2. 查看TimerTask.cancel()源码
public abstract class TimerTask implements Runnable { final Object lock = new Object(); public boolean cancel() { synchronized(lock) { boolean result = (state == SCHEDULED); state = CANCELLED; return result; } }
对于任务的取消操作,只是简单的修改一下任务状态,中途也只占有一个lock锁!接着看一下执行任务的线程逻辑。
class TimerThread extends Thread { private TaskQueue queue; public void run() { mainLoop(); } private void mainLoop() { while (true) { synchronized(queue) { while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); task = queue.getMin(); // 此处加task锁,防止其他线程同时调用task.cancel() synchronized(task.lock) { // ...维护闹钟状态 } } if (!taskFired) // 时间未到 queue.wait(executionTime - currentTime); } if (taskFired) // 执行闹钟时,没有保持任何锁 task.run(); } }
可以看到当TimerThead真正执行闹钟时,是没有持锁的,所以当闹钟正在运行的时候AlarmTask.cancel()对其是不起作用的,换言之,只能取消下一次将要执行的闹钟。
3. 查看Timer.purge()源码
public class Timer { private final TaskQueue queue = new TaskQueue(); // 保证被取消的task能及时进行垃圾回收 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; }
进一步查看queue.quickRemove(i)和queue.heapify()。
class TaskQueue { void quickRemove(int i) { queue[i] = queue[size]; queue[size--] = null; //清除无效任务,防止内存泄漏 } 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; } } void heapify() { for (int i = size/2; i >= 1; i--) fixDown(i); }
可以看到Timer.purge()在持有queue锁时主要做两件事
1.及时清除队列中无效的闹钟防止内存泄漏。
2.重新规整队列中闹钟。
4. 最后看一下Timer.cancel()源码
public class Timer { private final TaskQueue queue = new TaskQueue(); private final TimerThread thread = new TimerThread(queue); public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); //防止队列为空的情况下,TimerThead无限等待 queue.notify(); } }
该方法在清除所有闹钟的同时,与TimerThread发生了一次线程通信——唤醒TimerThread并让其永久退出。
private void mainLoop() { while (true) { synchronized(queue) { while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // TimerThread永久退出 queue.wait(executionTime - currentTime); } }}
以上是整个过程的静态分析,现在捕捉一个线程快照进行动态分析。为了dump一个特定时刻的线程快照,现在在Timer.sched()打一个断点(注意断点的方式与位置)。
以debug模式运行下面的例子。
public class ScheduleDemo { public static void main(String[] args) throws Exception { AlarmTask alarm1 = new AlarmTask("闹钟1"); AlarmTask alarm2 = new AlarmTask("闹钟2"); new Thread("线程1"){ public void run() { log.info("["+Thread.currentThread().getName()+"]调度闹钟1"); timer.schedule(alarm1,delay,period); } }.start(); new Thread("线程2"){ public void run() { log.info("["+Thread.currentThread().getName()+"]调度闹钟2"); timer.schedule(alarm2,delay,period); } }.start(); Thread.sleep(1500); new Thread("线程3"){ public void run() { log.info("["+Thread.currentThread().getName()+"]取消闹钟2"); alarm2.cancel(); } }.start(); new Thread("线程4"){ public void run() { log.info("["+Thread.currentThread().getName()+"]清理无用闹钟"); timer.purge(); } }.start(); new Thread("线程5"){ public void run() { log.info("["+Thread.currentThread().getName()+"]关闭所有闹钟"); timer.cancel(); } }.start(); } /** * 模拟闹钟 */ static class AlarmTask extends TimerTask{ String name ; public AlarmTask(String name){ this.name=name; } public void run() { log.info("["+Thread.currentThread().getName()+"]-["+name+"]嘀。。。"); Thread.sleep(1000); //模拟闹钟执行时间 } }}
下图是visualVM工具dump出的线程快照(断点处)
通过上面的快照可以看到,当“线程1“(持有两把锁)处于RUNNABLE状态时,”线程2“、“线程3”、“线程4”、“线程5”都处于BLOCKED状态。需要注意的是,因为TimerThread的时间未到,暂时处于WATING状态(等待唤醒)。
下面是一个简单的形象图
总结:Timer为了保证线程安全,使用了大量的锁机制,整体上对CPU的利用率不高。