Java定时任务Timer调度器【二】 多线程源码分析(图文版)

上一节通过一个小例子分析了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在)将任务加入队列的同时实现了内部排序。

扫描二维码关注公众号,回复: 4238761 查看本文章
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的利用率不高。

猜你喜欢

转载自my.oschina.net/u/3536632/blog/2960957