Java并发编程笔记之Timer源码分析

简介: timer在JDK里面,是很早的一个API了。具有延时的,并具有周期性的任务,在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做,但是Timer存在一些缺陷,为什么这么说呢?   Timer只创建唯一的线程来执行所有Timer任务。

timer在JDK里面,是很早的一个API了。具有延时的,并具有周期性的任务,在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做,但是Timer存在一些缺陷,为什么这么说呢?

  Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。例如一个TimerTask每10秒执行一次,而另外一个TimerTask每40ms执行一次,重复出现的任务会在后来的任务完成后快速连续的被调用4次,要么完全“丢失”4次调用。Timer的另外一个问题在于,如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。

 

这里做了一个小的 demo 来复现问题,代码如下:


package com.hjc;

import java.util.Timer;
import java.util.TimerTask;

/**
 * Created by cong on 2018/7/12.
 */
public class TimerTest {
    //创建定时器对象
    static Timer timer = new Timer();

    public static void main(String[] args) {
        //添加任务1,延迟500ms执行
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                System.out.println("---one Task---");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                throw new RuntimeException("error ");
            }
        }, 500);
        //添加任务2,延迟1000ms执行
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                for (;;) {
                    System.out.println("---two Task---");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }, 1000);

    }
}


如上代码先添加了一个任务在 500ms 后执行,然后添加了第二个任务在 1s 后执行,我们期望的是当第一个任务输出 ---one Task--- 后等待 1s 后第二个任务会输出 ---two Task---,

但是执行完毕代码后输出结果如下所示:

 

例子2,


public class Shedule {
    private static long start;

    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
                try{
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };

        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis()-start);
            }
        };

        Timer timer = new Timer();
        start = System.currentTimeMillis();
        //启动一个调度任务,1S钟后执行
        timer.schedule(task,1000);
        //启动一个调度任务,3S钟后执行
        timer.schedule(task1,3000);


    }

}


上面程序我们预想是第一个任务执行后,第二个任务3S后执行的,即输出一个1000,一个3000.

实际运行结果如下:

实际运行结果并不如我们所愿。世界结果,是过了4S后才输出第二个任务,即4001约等于4秒。那部分时间时间到哪里去了呢?那个时间是被我们第一个任务的sleep所占用了。

现在我们在第一个任务中去掉Thread.sleep();这一行代码,运行是否正确了呢?运行结果如下:

可以看到确实是第一个任务过了1S后执行,第二个任务在第一个任务执行完后过3S执行了。

这就说明了Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题

 

Timer 实现原理分析

下面简单介绍下 Timer 的原理,如下图是 Timer 的原理模型介绍:

1.其中 TaskQueue 是一个平衡二叉树堆实现的优先级队列,每个 Timer 对象内部有唯一一个 TaskQueue 队列。用户线程调用 timer 的 schedule 方法就是把 TimerTask 任务添加到 TaskQueue 队列,在调用 schedule 的方法时候 long delay 参数用来说明该任务延迟多少时间执行。

2.TimerThread 是具体执行任务的线程,它从 TaskQueue 队列里面获取优先级最小的任务进行执行,需要注意的是只有执行完了当前的任务才会从队列里面获取下一个任务而不管队列里面是否有已经到了设置的 delay 时间,一个 Timer 只有一个 TimerThread 线程,所以可知 Timer 的内部实现是一个多生产者单消费者模型。

 

从实现模型可以知道要探究上面的问题只需看 TimerThread 的实现就可以了,TimerThread 的 run 方法主要逻辑源码如下:


public void run() {
   try {
       mainLoop();
   } finally {
       // 有人杀死了这个线程,表现得好像Timer已取消
       synchronized(queue) {
           newTasksMayBeScheduled = false;
           queue.clear();  // 消除过时的引用
       }
   }
}
 private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                //从队列里面获取任务时候要加锁
                synchronized(queue) {
                    ......
                }
                if (taskFired)  
                    task.run();//执行任务
            } catch(InterruptedException e) {
            }
        }
 }


可知当任务执行过程中抛出了除 InterruptedException 之外的异常后,唯一的消费线程就会因为抛出异常而终止,那么队列里面的其他待执行的任务就会被清除。所以 TimerTask 的 run 方法内最好使用 try-catch 结构 catch 主可能的异常,不要把异常抛出到 run 方法外。

其实要实现类似 Timer 的功能使用 ScheduledThreadPoolExecutor 的 schedule 是比较好的选择。ScheduledThreadPoolExecutor 中的一个任务抛出了异常,其他任务不受影响的。

ScheduledThreadPoolExecutor 例子如下:


/**
 * Created by cong on 2018/7/12.
 */
public class ScheduledThreadPoolExecutorTest {
    static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);

    public static void main(String[] args) {

        scheduledThreadPoolExecutor.schedule(new Runnable() {

            public void run()  {
                System.out.println("---one Task---");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                throw new RuntimeException("error ");
            }

        }, 500, TimeUnit.MICROSECONDS);

        scheduledThreadPoolExecutor.schedule(new Runnable() {

            public void run() {
                for (int i =0;i<5;++i) {
                    System.out.println("---two Task---");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }

        }, 1000, TimeUnit.MICROSECONDS);

        scheduledThreadPoolExecutor.shutdown();
    }
}


运行结果如下:

之所以 ScheduledThreadPoolExecutor 的其他任务不受抛出异常的任务的影响是因为 ScheduledThreadPoolExecutor 中的 ScheduledFutureTask 任务中 catch 掉了异常,但是在线程池任务的 run 方法内使用 catch 捕获异常并打印日志是最佳实践。

目录
相关文章
|
2天前
|
安全 Java API
告别繁琐编码,拥抱Java 8新特性:Stream API与Optional类助你高效编程,成就卓越开发者!
【8月更文挑战第29天】Java 8为开发者引入了多项新特性,其中Stream API和Optional类尤其值得关注。Stream API对集合操作进行了高级抽象,支持声明式的数据处理,避免了显式循环代码的编写;而Optional类则作为非空值的容器,有效减少了空指针异常的风险。通过几个实战示例,我们展示了如何利用Stream API进行过滤与转换操作,以及如何借助Optional类安全地处理可能为null的数据,从而使代码更加简洁和健壮。
16 0
|
1天前
|
存储 安全 Java
【数据保护新纪元】Java编程:揭秘黑客攻击背后的防线,打造坚不可摧的安全堡垒!
【8月更文挑战第30天】本文全面介绍了Java安全性编程的基本概念和实战技巧,涵盖输入验证、错误处理、数据加密、权限控制及安全审计等方面。通过具体示例,帮助开发者有效预防安全风险,提升程序的稳定性和可靠性,保护用户数据安全。适合希望提升Java应用安全性的开发者参考。
13 4
|
1天前
|
IDE Java 测试技术
揭秘Java高效编程:测试与调试实战策略,让你代码质量飞跃,职场竞争力飙升!
【8月更文挑战第30天】在软件开发中,测试与调试对确保代码质量至关重要。本文通过对比单元测试、集成测试、调试技巧及静态代码分析,探讨了多种实用的Java测试与调试策略。JUnit和Mockito分别用于单元测试与集成测试,有助于提前发现错误并提高代码可维护性;Eclipse和IntelliJ IDEA内置调试器则能快速定位问题;Checkstyle和PMD等工具则通过静态代码分析发现潜在问题。综合运用这些策略,可显著提升代码质量,为项目成功打下坚实基础。
|
1天前
|
Java 开发者 UED
【揭秘Java编程新境界】事件驱动:如何在Java中捕捉每一个关键瞬间?
【8月更文挑战第30天】事件驱动编程是一种编程范式,使程序能在事件发生时响应,而非按严格顺序执行。本文介绍Java中的事件驱动编程,包括基本概念、优势及其实现方法。通过事件监听器和事件对象,Java能够高效处理GUI、网络编程和游戏开发中的各种事件。文中还提供了创建事件监听器、自定义事件及处理多个事件源的示例代码,帮助读者更好地理解和应用这一强大的编程范式。
|
3天前
|
Java 程序员 编译器
Java编程中的异常处理:理解与实践
【8月更文挑战第28天】在Java的世界中,异常是程序运行中不可避免的一部分。它们像是旅途中的路障,挑战着程序员的智慧和耐心。本文将带你走进Java的异常处理机制,从基础概念到高级应用,我们将一起探索如何优雅地处理这些不请自来的挑战者。你将学习到如何捕获、处理以及预防异常,确保你的代码像经验丰富的旅行者一样,即使在最崎岖的道路上也能从容前行。
|
3天前
|
存储 Java
Java编程中的对象序列化与反序列化
【8月更文挑战第28天】在Java世界中,对象序列化与反序列化是数据持久化和网络传输的关键技术。本文将深入浅出地探讨这一过程,带你领略其背后的原理及应用,让你的程序在数据的海洋中自由航行。
|
2天前
|
算法 Java 程序员
探索代码之美——从小白到大牛的编程旅程深入理解Java中的异常处理机制
【8月更文挑战第29天】编程,一门看似神秘而复杂的艺术,实则充满无限可能与创造的乐趣。本文将带你走进编程的世界,从最初的迷茫与困惑,到逐步掌握编程思维,再到深入理解代码背后的哲学,一起见证一个编程小白如何成长为技术大牛的奇妙旅程。
|
2天前
|
机器学习/深度学习 Java TensorFlow
深度学习中的图像识别:从理论到实践Java中的多线程编程入门指南
【8月更文挑战第29天】本文将深入探讨深度学习在图像识别领域的应用,从基础理论到实际应用案例,带领读者一步步理解如何利用深度学习技术进行图像识别。我们将通过一个简单的代码示例,展示如何使用Python和TensorFlow库实现一个基本的图像识别模型。无论你是初学者还是有一定经验的开发者,都能从中获得启发和学习。 【8月更文挑战第29天】在Java世界里,线程是程序执行的最小单元,而多线程则是提高程序效率和响应性的关键武器。本文将深入浅出地引导你理解Java多线程的核心概念、创建方法以及同步机制,帮助你解锁并发编程的大门。
|
2天前
|
安全 Java API
Java 8 流库的魔法革命:Filter、Map、FlatMap 和 Optional 如何颠覆编程世界!
【8月更文挑战第29天】Java 8 的 Stream API 通过 Filter、Map、FlatMap 和 Optional 等操作,提供了高效、简洁的数据集合处理方式。Filter 用于筛选符合条件的元素;Map 对元素进行转换;FlatMap 将多个流扁平化合并;Optional 安全处理空值。这些操作结合使用,能够显著提升代码的可读性和简洁性,使数据处理更为高效和便捷。
10 0
|
3天前
|
Java 程序员 UED
Java编程中的异常处理最佳实践
【8月更文挑战第28天】在Java的世界里,异常处理是代码健壮性的守护神。它不仅保护程序免受意外错误的侵袭,还确保用户体验的连贯性。本文将引导你穿梭于异常处理的迷宫,揭示捕获、处理及预防异常的艺术。从基本的try-catch语句到精细的异常层次结构,我们将一同探索如何优雅地应对错误,使程序即使在逆境中也能展现出它的韧性和魅力。
下一篇
云函数