Java多线程案例之定时器

简介: Java多线程案例之定时器

一. 定时器概述

1. 什么是定时器

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

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

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

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

2. 标准库中的定时器

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

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

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

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

public abstract class TimerTask implements Runnable // jdk源码
序号 方法 解释
1 public void schedule(TimerTask task, long delay) 指定任务, 延迟多久执行该任务
2 public void schedule(TimerTask task, Date time) 指定任务, 指定任务的执行时间
3 public void schedule(TimerTask task, long delay, long period) 连续执行指定任务, 延迟时间, 连续执行任务的时间间隔, 毫秒为单位
4 public void schedule(TimerTask task, Date firstTime, long period) 连续执行指定任务, 第一次任务的执行时间, 连续执行任务的时间间隔
5 public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 与方法4作用相同
6 public void scheduleAtFixedRate(TimerTask task, long delay, long period) 与方法3作用相同
7 public 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);
    }
}

执行结果:

73d8c9be8b2a4960a39693770de0ac9a.png

观察执行结果, 任务执行结束后程序并没有结束, 即进程并没有结束, 这是因为上面的代码定时器内部是开启了一个线程去执行任务的, 虽然任务执行完成了, 但是该线程并没有销毁; 这和自己定义一个线程执行完成 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);
    }
}

执行结果:73d8c9be8b2a4960a39693770de0ac9a.png

目录
相关文章
|
11天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
13天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
13天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
5月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
144 1
|
8月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
246 2
|
8月前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
77 1
|
5月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
89 6
|
5月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
95 5
|
5月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
92 3
|
6月前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
72 1