聊聊jdk自带定时任务Timer:使用方式、源码解析与优缺点

597 阅读4分钟

这里是小奏,觉得文章不错可以关注公众号小奏技术

Timer介绍

如果你想要定时执行任务,那么jdk早期提供给你的定时器类就只有Timer

Timer是jdk1.3引入的用来执行定时任务的类.它可以在指定的时间执行任务,也可以周期性的执行任务.

使用方式

这里我直接通过代码演示

		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM dd:HH:mm:ss");
		java.util.Timer jdkTimer = new java.util.Timer();
		jdkTimer.scheduleAtFixedRate(new java.util.TimerTask() {
			@Override
			public void run() {
				// do something
				System.out.println("time: " + LocalDateTime.now().format(formatter) + " xiao zou jdk timer task");
			}
		}, 2000, 1000);  // 2s 后调度 周期为 1s

这里创建了一个Timer对象,然后调用scheduleAtFixedRate方法,传入一个TimerTask对象,然后指定了延迟时间和周期时间.

就可以进行定时调度了

scheduleAtFixedRate方法有三个参数

public void scheduleAtFixedRate(TimerTask task, long delay, long period) {}
  • TimerTask 定时任务
  • delay 延迟时间
  • period 周期时间

如果period为0,那么就是执行一次。调用schedule的下面这个方法即可

public void schedule(TimerTask task, long delay) {}

Timer执行定时任务主要有四个方法

  • public void schedule(TimerTask task, long delay, long period) 延迟delay毫秒后执行task,之后每隔period毫秒执行一次task
  • public void schedule(TimerTask task, Date firstTime, long period)firstTime时间执行task,之后每隔period毫秒执行一次task
  • public void scheduleAtFixedRate(TimerTask task, long delay, long period) 延迟delay毫秒后执行task,之后每隔period毫秒执行一次task
  • public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)firstTime时间执行task,之后每隔period毫秒执行一次task

方法1和方法3实现功能一样,方法2和方法4功能一样

不同的如果前面任务延时了,后面任务到达指定间隔时间period后任务的处理方式不一样

下面我们来基于例子来详细看看

schedule和scheduleAtFixedRate区别

举个简单例子 比如你1s要吃一个蛋糕,但是你有一次吃一个蛋糕吃了10s,那么接下来10s之后,你有两个选择

  • 你继续每秒吃一个蛋糕
  • 把浪费的10s内应该吃的的蛋糕补回来呢,应该延期内要吃11个蛋糕,所以你要在1s内吃11个蛋糕

schedule

		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM dd:HH:mm:ss");
		java.util.Timer jdkTimer = new java.util.Timer();
		AtomicInteger count = new AtomicInteger(0);
		
		jdkTimer.schedule(new java.util.TimerTask() {
			@Override
			public void run() {
				System.out.println("xiazou 吃蛋糕开始!!, time: " + LocalDateTime.now().format(formatter));
				try {
					if (count.get() == 0) {
						TimeUnit.SECONDS.sleep(10);
						count.incrementAndGet();
					}
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
				System.out.println("xiazou 吃完了一个蛋糕, time: " + LocalDateTime.now().format(formatter));
			}

		}, 1000, 1000);

运行结果

可以看到schedule属于第一种,不管你前面任务延时了多久,后面任务都是按照指定的周期时间执行

scheduleAtFixedRate

		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM dd:HH:mm:ss");
		java.util.Timer jdkTimer = new java.util.Timer();
		AtomicInteger count = new AtomicInteger(0);
		
		jdkTimer.scheduleAtFixedRate(new java.util.TimerTask() {
			@Override
			public void run() {
				System.out.println("xiazou 吃蛋糕开始!!, time: " + LocalDateTime.now().format(formatter));
				try {
					if (count.get() == 0) {
						TimeUnit.SECONDS.sleep(10);
						count.incrementAndGet();
					}
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
				System.out.println("xiazou 吃完了一个蛋糕, time: " + LocalDateTime.now().format(formatter));
			}

		}, 1000, 1000);

运行结果

可以看到scheduleAtFixedRate属于第二种,如果前面任务延时了,那么后面任务会在延时的基础上继续执行

Timer源码分析

其实上面有一个疑问,如果任务执行延期,后面的任务到了指定时间,为什么不继续开个线程来执行接下来的任务呢?

因为Timer内部只有一个线程,只有一个TimerThread线程

TimerThread

任务存储使用的是TaskQueue

TaskQueue

TaskQueue底层存储就只有一个数组。但实际是一个二叉堆(小顶堆)

新增数据会进行fixUp操作

  • add
    void add(TimerTask task) {
        // Grow backing store if necessary
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        fixUp(size);
    }

fixUp

    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
    void removeMin() {
        queue[1] = queue[size];
        queue[size--] = null;  // Drop extra reference to prevent memory leak
        fixDown(1);
    }

数据删除会进行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;
        }
    }

Timer缺点

  • 单线程执行任务,如果任务执行时间过长,会影响后续任务的执行
  • 底层数据存储是二叉堆,添加任务和删除任务的时间复杂度是O(logn),不是特别高效
  • API相对简单,不够灵活

总结

Timer作为jdk原生的定时器,功能相对简单。

是单线程的,耗时任务会影响后续任务的执行

存储任务数据结构为二叉堆,添加和删除任务的时间复杂度为O(logn),不是特别高效