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

目录
相关文章
|
4天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
|
3天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
26 1
|
11天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
17天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
44 9
|
17天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
Java
Java中需要注意的一些案例
Java中需要注意的一些案例
119 0
|
13天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
21天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
4天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####