当前位置:主页 > java教程 > Java定时器

Java多线程案例之定时器详解

发布:2023-04-20 15:40:01 59


给网友朋友们带来一篇相关的编程文章,网友步康安根据主题投稿了本篇教程内容,涉及到Java多线程、定时器、Java、定时器、Java、多线程、Java定时器相关内容,已被472网友关注,下面的电子资料对本篇知识点有更加详尽的解释。

Java定时器

一. 定时器概述

1. 什么是定时器

定时器是一种实际开发中非常常用的组件, 类似于一个 “闹钟”, 达到一个设定的时间之后, 就执行某个指定好的代码.

比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.

比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).

类似于这样的场景就需要用到定时器.

2. 标准库中的定时器

标准库中提供了一个 Timer 类, Timer 类的核心方法为schedule.

Timer类构造时内部会创建线程, 有下面的四个构造方法, 可以指定线程名和是否将定时器内部的线程指定为后台线程(即守护线程), 如果不指定, 定时器对象内部的线程默认为前台线程.

序号构造方法解释
1public Timer()无参, 定时器关联的线程为前台线程, 线程名为默认值
2public Timer(boolean isDaemon)指定定时器中关联的线程类型, true(后台线程), false(前台线程)
3public Timer(String name)指定定时器关联的线程名, 线程类型为前台线程
4public Timer(String name, boolean isDaemon) 指定定时器关联的线程名和线程类型

schedule 方法是给Timer注册一个任务, 这个任务在指定时间后进行执行, TimerTask类就是专门描述定时器任务的一个抽象类, 它实现了Runnable接口.

public abstract class TimerTask implements Runnable // jdk源码
序号方法解释
1public void schedule(TimerTask task, long delay)指定任务, 延迟多久执行该任务
2public void schedule(TimerTask task, Date time)指定任务, 指定任务的执行时间
3public void schedule(TimerTask task, long delay, long period)连续执行指定任务, 延迟时间, 连续执行任务的时间间隔, 毫秒为单位
4public void schedule(TimerTask task, Date firstTime, long period)连续执行指定任务, 第一次任务的执行时间, 连续执行任务的时间间隔
5public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)与方法4作用相同
6public void scheduleAtFixedRate(TimerTask task, long delay, long period)与方法3作用相同
7public void cancel()清空任务队列中的全部任务, 正在执行的任务不受影响

代码示例:

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

public class TestProgram {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行延后3s的任务!");
            }
        }, 3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行延后2s后的任务!");
            }
        }, 2000);
        
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行延后1s的任务!");
            }
        }, 1000);
    }
}

执行结果:

观察执行结果, 任务执行结束后程序并没有结束, 即进程并没有结束, 这是因为上面的代码定时器内部是开启了一个线程去执行任务的, 虽然任务执行完成了, 但是该线程并没有销毁; 这和自己定义一个线程执行完成 run 方法后就自动销毁是不一样的, Timer 本质上是相当于线程池, 它缓存了一个工作线程, 一旦任务执行完成, 该工作线程就处于空闲状态, 等待下一轮任务.

二. 定时器的简单实现

首先, 我们需要定义一个类, 用来描述一个定时器当中的任务, 类要成员要有一个Runnable, 再加上一个任务执行的时间戳, 具体还包含如下内容:

  • 构造方法, 用来指定任务和任务的延迟执行时间.
  • 两个get方法, 分别用来给外部对象获取该对象的任务和执行时间.
  • 实现Comparable接口, 指定比较方式, 用于判断定时器任务的执行顺序, 每次需要执行时间最早的任务.
class MyTask implements Comparable<MyTask>{
    //要执行的任务
    private Runnable runnable;
    //任务的执行时间
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取当前任务的执行时间
    public long getTime() {
        return this.time;
    }
    //执行任务
    public void run() {
        runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}

然后就需要实现定时器类了, 我们需要使用一个数据结构来组织定时器中的任务, 需要每次都能将时间最早的任务找到并执行, 这个情况我们可以考虑用优先级队列(即小根堆)来实现, 当然我们还需要考虑线程安全的问题, 所以我们选用优先级阻塞队列 PriorityBlockingQueue 是最合适的, 特别要注意在自定义的任务类当中要实现比较方式, 或者实现一下比较器也行.

private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

我们自己实现的定时器类中要有一个注册任务的方法, 用来将任务插入到优先级阻塞队列中;

还需要有一个线程用来执行任务, 这个线程是从优先级阻塞队列中取出队首任务去执行, 如果这个任务还没有到执行时间, 那么线程就需要把这个任务再放会队列当中, 然后线程就进入等待状态, 线程等待可以使用sleep和wait, 但这里有一个情况需要考虑, 当有新任务插入到队列中时, 我们需要唤醒线程重新去优先级阻塞队列拿队首任务, 毕竟新注册的任务的执行时间可能是要比前一阵拿到的队首任务时间是要早的, 所以这里使用wait进行进行阻塞更合适, 那么唤醒操作就需要使用notify来实现了.

实现代码如下:

//自己实现的定时器类
class MyTimer {
    //扫描线程
    private Thread t = null;
    //阻塞队列,存放任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    public MyTimer() {
        //构造扫描线程
        t = new Thread(() -> {
           while (true) {
               //取出队首元素,检查队首元素执行任务的时间
               //时间没到,再把任务放回去
               //时间到了,就执行任务
               try {
                   synchronized (this) {
                       MyTask task = queue.take();
                       long curTime = System.currentTimeMillis();
                       if (curTime < task.getTime()) {//时间没到,放回去queue.put(task);//放回任务后,不应该立即就再次取出该任务//所以wait设置一个阻塞等待,以便新任务到时间或者新任务来时后再取出来this.wait(task.getTime() - curTime);
                       } else {//时间到了,执行任务task.run();
                       }
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);

               }
           }
        });
        t.start();
    }

    /**
     * 注册任务的方法
     * @param runnable 任务内容
     * @param after 表示在多少毫秒之后执行. 形如 1000
     */
    public void schedule (Runnable runnable, long after) {
        //获取当前时间的时间戳再加上任务时间
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);
        queue.put(task);
        //每次当新任务加载到阻塞队列时,需要中途唤醒线程,因为新进来的任务可能是最早需要执行的
        synchronized (this) {
            this.notify();
        }
    }
}

要注意上面扫描线程中的synchronized并不能只要针对wait方法加锁, 如果只针对wait加锁的话, 考虑一个极端的情况, 假设的扫描线程刚执行完put方法, 这个线程就被cpu调度走了, 此时另有一个线程在队列中插入了新任务, 然后notify唤醒了线程, 而刚刚并没有执行wait阻塞, notify就没有起到什么作用, 当cpu再调度到这个线程, 这样的话如果新插入的任务要比原来队首的任务时间更早, 那么这个新任务就被错过了执行时间, 这些线程安全问题真是防不胜防啊, 所以我们需要保证这些操作的原子性, 也就是上面的代码, 扩大锁的范围, 保证每次notify都是有效的.

那么最后基于上面的代码, 我们来测试一下这个定时器:

public class TestDemo23 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2s后执行的任务1");
            }
        }, 2000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2s后执行的任务1");
            }
        }, 1000);
    }
}

执行结果:

到此这篇关于Java多线程案例之定时器详解的文章就介绍到这了,更多相关Java定时器内容请搜索码农之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持码农之家!


参考资料

相关文章

  • 如何理解与使用Java弱引用(WeakReference)

    发布:2020-01-10

    这篇文章主要介绍了Java弱引用(WeakReference)的理解与使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧


  • Java Big Number操作BigInteger及BigDecimal类详解

    发布:2022-10-10

    为网友们分享了关于Java的教程,这篇文章主要为大家介绍了Java Big Number操作BigInteger及BigDecimal类详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪


  • java中用ObjectMapper类实现Json与bean的转换示例

    发布:2022-10-26

    给大家整理一篇关于java的教程,这篇文章主要给大家介绍了关于在java中用ObjectMapper类实现Json与bean转换的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面跟着


  • java中int初始化可以为0,但不能为NULL问题

    发布:2023-03-23

    这篇文章主要介绍了java中int初始化可以为0,但不能为NULL问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教


  • Java常用内置注解的实例内容

    发布:2020-01-16

    这篇文章主要介绍了Java常用内置注解用法,结合实例形式分析了java使用@SuppressWarnings关闭警告信息以及@Depreca标注的元素不使用两种注解使用方法,需要的朋友可以参考下


  • 浅谈java什么时候需要用序列化 

    发布:2023-04-26

    本文主要介绍了浅谈java什么时候需要用序列化,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧


  • 详解Java如何实现在PDF中插入,替换或删除图像

    发布:2023-03-06

    图文并茂的内容往往让人看起来更加舒服,如果只是文字内容的累加,往往会使读者产生视觉疲劳。搭配精美的文章配图则会使文章内容更加丰富。那我们要如何在PDF中插入、替换或删除图像呢?别担心,今天为大家介绍一种高效便捷的方法


  • 如何解决Java中invokedynamic字节码指令问题

    发布:2020-02-05

    这篇文章主要介绍了Java中invokedynamic字节码指令问题,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下


网友讨论