多线程案例-定时器(附完整代码)

简介: 多线程案例-定时器(附完整代码)

定时器是什么

定时器是软件开发中的一个重要组件.类似于一个"闹钟".达到一个设定的时间之后,就执行某个指定好的代码.

定时器是一种实际开发中非常常用的组件.

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

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

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

标准库中的定时器

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

schedule包含两个参数.第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)

下面是代码示例:

public class TestTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        }, 3000);
 
        System.out.println("hello main");
    }
}

运行代码,我们可以观察到一上来直接打印"hello main",然后三秒之后执行了"hello timer",为什么会出现这种情况呢?显然是每个Timer内置了一个线程.

注:由于Timer内置了线程(前台线程),会阻止进程结束.这是因为timer不知道代码是否还会添加新的任务进来,就处于一种严阵以待的状态.

所以此处需要使用cancel()方法来主动结束,否则timer不知道其它什么地方还会添加任务.

public class TestTimer {
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        }, 3000);
 
        System.out.println("hello main");
        Thread.sleep(4000);
        timer.cancel();
    }
}

这时就可以观察到进程结束成功了.

实现定时器

定时器的构成

一个带优先级队列(不要使用PriorityBlockingQueue,容易死锁!),而是使用PriorityQueue

实现原理:

1.队列中每个元素是一个Task对象.

2.Task中带有时间属性,队首元素就是即将要执行的任务

3.同时有一个worker线程一直在扫描队首元素,看队首元素是否需要执行(先执行时间小的,后执行时间大的).

1.Timer类提供的核心接口为schedule,用于注册一个任务,并指定这个任务多长时间后执行.

public class MyTimer {
    public void schedule(Runnable runnable, long time) {
        //TODO
    }
}

2.Task类用于描述一个任务(作为Timer的内部类).里面包含着一个Runnable对象和一个time(毫秒时间戳)

这个对象需要放到优先级队列中,因此需要实现Comparable接口.

PriorityQueue, TreeMap, TreeSet都要求元素是"可比较大小的".需要Comparable,Comparator.

HashMap, HashSet则是要求元素是"可比较相等的","可hash的".因此需要实现

equals,hashCode方法.

class MyTimerTask implements Comparable<MyTimerTask> {
    private Runnable runnable;
    //为了方便后续判定,使用了绝对的时间戳
    private long time;
    
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //取当前时刻的时间戳 + delay,作为该任务实际执行的时间戳
        this.time = System.currentTimeMillis() + delay;
    }
 
    @Override
    public int compareTo(MyTimerTask o) {
        //这样的写法意味着每次去除的是时间最小的元素
        //到底是谁减谁,不要记,建议随便写个试一试
        return (int)(this.time - o.time);
    }
}

3.Timer实例中,通过PriorityQueue来组织若干个Task对象.

通过schedule往队列中插入一个个Task对象

class MyTimer {
    //核心结构
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //创建一个锁对象
    private Object locker = new Object();
    
    public void schedule(Runnable runnable, long time) {
        //根据参数,构造MyTimerTask,插入队列即可
        synchronized(locker) {
            MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
            queue.offer(myTimerTask);
            locker.notify();
        }
    }
}

4.Timer类中存在一个worker线程,一直不停地扫描队首元素,看看是否能执行这个任务.

所谓能执行就是时间已经到达了.

//在这里构建线程,负责执行具体的任务了
public MyTimer() {
    Thread t = new Thread(() -> {
        while(true) {
            try {
                synchronized(locker) {
                    //阻塞队列,只有阻塞的入队列和阻塞的出队列,没有阻塞查看队首元素
                    while(!queue.isEmpty()) {
                        locker.wait();
                    }
                    MyTimerTask myTimerTask = queue.peek();
                    long curTime = System.currentTimeMillis();
                    if(curTime >= myTimerTask.getTime()) {
                        //时间到了,可以执行任务了
                        queue.poll();
                        myTimerTask.run();
                    } else {
                        //时间还没到
                        locker.wait(myTimerTask.time - curTime);
                    }
                }
            } catch(InterruptException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}

下面附上完整代码以及解析:

import java.util.PriorityQueue;
 
//通过这个类,来描述一个任务,这一整个任务,是要放到优先级队列中的
class MyTimerTask implements Comparable<MyTimerTask>{
    //在什么时间来执行这个任务
    //此处的time是一个ms级别的时间戳
    private long time;
    //实际任务要执行的代码
    private Runnable runnable;
    //delay期望是一个相对时间
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //计算一下要执行任务的绝对时间.(使用绝对时间,方便判定任务是否到达时间的)
        this.time = System.currentTimeMillis() + delay;
    }
 
    //实际执行代码的方法(一般在主函数中重写)
    public void run() {
        runnable.run();
    }
 
    //用于获得任务执行时间
    public long getTime() {
        return this.time;
    }
 
    //构建一个比较器(因为在优先级队列中是按时间对任务进行比较)
    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
}
 
public class MyTimer{
    //构建一个线程
    private Thread t = null;
    //创建存放任务的主体--优先级队列
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //创建一个锁对象
    private Object locker = new Object();
 
    //结束进程的方法
    public void cancel() {
        t.interrupt();
    }
    //构造方法:以用于创建线程
    public MyTimer() {
        t = new Thread(() -> {
            while(!Thread.interrupted()) {
                try {
                    synchronized (locker) {
                        while(queue.isEmpty()) {
                            //当队列为空时,这个线程就一直处于阻塞等待的状态
                            locker.wait();
                        }
                        //获得队列头部元素
                        MyTimerTask task = queue.peek();
                        //计算系统当前时间
                        long curTime = System.currentTimeMillis();
                        //判断是否应该执行
                        if(curTime >= task.getTime()) {
                            //执行任务
                            queue.poll();
                            task.run();
                        } else {
                            //等待到指定时间再执行任务
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        t.start();
    }
    //通过这个方法来执行实际的任务
    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            //先创建一个任务,后将任务放入队列
            MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
            queue.offer(myTimerTask);
            //唤醒因为队列中因为没有任务而阻塞等待的线程
            locker.notify();
        }
    }
}

 

相关文章
|
4月前
|
安全 Python
告别低效编程!Python线程与进程并发技术详解,让你的代码飞起来!
【7月更文挑战第9天】Python并发编程提升效率:**理解并发与并行,线程借助`threading`模块处理IO密集型任务,受限于GIL;进程用`multiprocessing`实现并行,绕过GIL限制。示例展示线程和进程创建及同步。选择合适模型,注意线程安全,利用多核,优化性能,实现高效并发编程。
71 3
|
20天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
14 1
|
1月前
|
安全 Java
【多线程-从零开始-拾】Timer-定时器
【多线程-从零开始-拾】Timer-定时器
30 0
|
2月前
|
安全 Java 调度
python3多线程实战(python3经典编程案例)
该文章提供了Python3中多线程的应用实例,展示了如何利用Python的threading模块来创建和管理线程,以实现并发执行任务。
38 0
|
3月前
|
Java Windows
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
|
3月前
|
消息中间件 安全 Kafka
"深入实践Kafka多线程Consumer:案例分析、实现方式、优缺点及高效数据处理策略"
【8月更文挑战第10天】Apache Kafka是一款高性能的分布式流处理平台,以高吞吐量和可扩展性著称。为提升数据处理效率,常采用多线程消费Kafka数据。本文通过电商订单系统的案例,探讨了多线程Consumer的实现方法及其利弊,并提供示例代码。案例展示了如何通过并行处理加快订单数据的处理速度,确保数据正确性和顺序性的同时最大化资源利用。多线程Consumer有两种主要模式:每线程一个实例和单实例多worker线程。前者简单易行但资源消耗较大;后者虽能解耦消息获取与处理,却增加了系统复杂度。通过合理设计,多线程Consumer能够有效支持高并发数据处理需求。
164 4
|
3月前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
3月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
43 0
|
5月前
|
Java
【代码诗人】Java线程的生与死:一首关于生命周期的赞歌!
【6月更文挑战第19天】Java线程生命周期,如诗般描绘了从新建到死亡的旅程:创建后待命,`start()`使其就绪,获得CPU则运行,等待资源则阻塞,任务完或中断即死亡。理解生命周期,善用锁、线程池,优雅处理异常,确保程序高效稳定。线程管理,既是艺术,也是技术。
30 3
|
5月前
|
Java
【代码诗人】Java线程的生与死:一首关于生命周期的赞歌!
【6月更文挑战第19天】在Java中,线程经历新建、就绪、运行、阻塞和死亡5个阶段。通过`start()`从新建转为就绪,进而可能运行;阻塞可能因等待资源;完成任务或中断后死亡。管理线程生命周期涉及合理使用锁、线程池、异常处理和优雅关闭,如使用`volatile`和中断标志。了解这些,能提升程序效率和稳定性。
27 2