多线程(初阶八:计时器Timer)

简介: 多线程(初阶八:计时器Timer)



一、标准库中的计时器

1、计时器的概念

计时器类似闹钟,有定时的功能,闹钟是到时间就会响,而计时器是到时间就会执行某一操作,可以指定时间,去执行某一任务(某一代码)。

2、计时器的简单介绍

在标准库中,提供了Timer类,Timer类的核心方法是schedule,里面包含两个参数,一个是要执行的任务代码,一个是设置多久之后执行这个任务代码的时间注意:Timer内置了线程(前台线程)

代码演示:

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

执行结果:

可以看到先打印 hello main ,等过了1s才打印 hello 1000,往后继续推,说明Timer内置了线程,main线程不用等待,而timer类是要到时间才会执行任务代码。注意:这里的线程并没有结束,可以看到idea里也没有显示线程结束,说明timer类里面内置的是前台线程。

但是timer类里面有cancel方法,可以结束线程,我们把这个方法加到打印hello 3000那方法里面,这样就可以结束timer类里面的线程了。

代码:

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

执行结果:

可以结束线程。


二、模拟实现一个计时器

1、思路

(1)计数器中要存放任务的数据结构

首先,我们知道,计时器是可以定时去执行一些任务操作,那么我们怎么每次先去执行时间小的那一操作呢?用数组吗?其实在某一些场景下确实可以用数组,但这就需要我们每次都去遍历数组,找出最小的时间,但是如果我们要定时很多任务,成千上万呢?这就不合理了,从数组里面找出这个时间最小的数据,一方面要考虑资源花销大的问题,还有要考虑时间的问题,找的时间太长,错过了已经到时要执行的任务,这说明,使用数组存放任务是不合理的。

可以用优先级队列,这样,每次拿都能拿到时间最小的任务,时间复杂度也仅仅是O(1),但是优先级队列不能是阻塞队列,不然会引起死锁问题。

(2)存放优先级队列中的类型:自定义任务类MyTimerTask

任务类是放要执行的代码和要执行任务时间,单独作为一类,存进优先级队列中,其中,优先级队列里的比较是按任务类的时间大小来比较的。

(3)计数器类MyTimer

里面有一个线程,放在MyTimer类的构造方法中,这个线程就是扫描线程,而这个扫描线程来完成判断和操作,入队列或者判断啥时候才执行要执行的代码的操作;还有创建任务schedule的方法,里面也有入队列的操作。

代码:

MyTimer类:
//通过这个类表示定时器
class MyTimer {
    Object locker = new Object();
    //负责扫描任务队列,执行任务的线程
    private Thread t = null;
    //任务队列
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //入队列的方法
    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            locker.notify();
        }
    }
    //构造方法,会创建线程,让扫描线程来完成判定和执行
    public MyTimer() {
        t = new Thread(() -> {
            while (true) {
                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) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

里面的核心代码:schedule方法,这是创建任务,里面包含了要执行的代码和执行代码的时间,还有就是构造方法,里面有一个线程,这个线程就是不断去判断队列有没有任务,到时间了的任务就拿队伍里时间最小的任务,执行这任务里的代码,没到时间就要等。

MyTimerTask任务类:

代码:

//通过这个类,来描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
    //在什么时间点来执行任务
    private long time;//此处的time是一个ms级别的时间戳
    //实际任务要执行的代码
    private Runnable runnable;
    public MyTimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //计算的是要执行任务绝对时间,方便判断是否到达时间了
        this.time = System.currentTimeMillis() + delay;
    }
    //得到要执行任务时间的方法
    public long getTime() {
        return this.time;
    }
    public void run() {
        this.runnable.run();
    }
    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
}

任务类里面放要是和执行的代码,和要执行代码的时间,因为要放进队列里,所以要实现一个比较器,用时间来比较,重写compareTo方法。

2、分析计时器的线程安全问题

(1)维护队列进出的操作

我们知道,不创建其他线程,就一个主线程去调用MyTimer类的话,一共就会有两个线程:主线程和 t 线程,这时候,主线程的代码是这样的

代码:

public class TimerTest {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        }, 2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        }, 1000);
        System.out.println("hello main");
    }
}

主线程有入队列的操作,但 t 线程也有出队列的操作,如图:

多线程操作一个队列有进有出,肯定是线程不安全的操作;所以,要维护这个队列,就要把入队列和出队列操作都上锁,同一时间要么只能入队列,要么只能出队列;

入队列操作上锁位置好知道,把创建任务和入队列操作都上锁;但是出队列呢?要在哪里上锁,把while循环都给上锁了?显然,这样的代码感觉有点危险,在这场景上确实可以用,但是,如果是在其他场景下,如果一个线程拿到锁了,但是因为没有实际来执行代码,就会不停的解锁、加锁,这样其他线程就饿死了,所以,还是在while里面,把里面的操作给上锁,这样看着没那么膈应。

如图的代码是最终版本的,上面的代码都是最终版本的。

(2)当队列是空的,就要阻塞等待

如图:

(3)如果没到时间,就要等待到时在执行要执行的代码

没到时间,就要阻塞等待,等待时间是: 要行的时间 - 现在的时间,没有限制要等待的时间的话,就会一直循环,每次循环判断是不是到时间了,因为循环这个代码执行速度是很快的,这样就会盲等,虽然计算机是在忙,都是在瞎忙活,所以代码要写出这样子,如图:


以上的代码都是计时器完整的版本,都看到这了,点个赞再走吧,谢谢谢谢谢!!!

相关文章
|
3月前
|
存储 安全 Java
线程池和计时器
线程池是一种线程使用模式,维护多个线程以分配并执行任务,避免频繁创建与销毁线程带来的开销。核心原理包括:创建空线程池、按需创建线程执行任务并归还线程池、以及任务排队等待。Java 标准库中的 `ThreadPoolExecutor` 实现了线程池功能,其构造方法涉及核心线程数、最大线程数、非核心线程空闲时间等参数。当任务超出处理能力时,线程池会采用不同的拒绝策略。此外,定时器通过 `Timer` 和 `TimerTask` 类实现定时执行任务的功能。自定义线程池和定时器可通过优先级队列和线程安全机制实现高效的任务调度。
44 5
|
2月前
|
安全 Java
【多线程-从零开始-拾】Timer-定时器
【多线程-从零开始-拾】Timer-定时器
36 0
|
7月前
|
Java
【Java多线程】定时器Timer
【Java多线程】定时器Timer
56 0
【Java多线程】定时器Timer
|
druid 关系型数据库 MySQL
高并发下 MySQL Statement Cancellation Timer 的线程数暴涨
高并发下 MySQL Statement Cancellation Timer 的线程数暴涨
【JavaEE】多线程之定时器(Timer)
【JavaEE】多线程之定时器(Timer)
|
安全 Java
【Java|多线程与高并发】定时器(Timer)详解
在Java中,定时器Timer类是用于执行定时任务的工具类。它允许你安排一个任务在未来的某个时间点执行,或者以固定的时间间隔重复执行。
|
Java 测试技术
在多线程中自定义实现定时器(Timer)
在多线程中自定义实现定时器(Timer)
【Java多线程】定时器Timer
在任务的执行时间未到之前,可能判断次数很多,比较耗费CPU,而且没有必要一值判断,只需在一定时间内进行判断执行时间到没到即可,所以在还没有到执行时间时,使用wait(时间)来让该线程进行等待,在创建任务时唤醒等待即可,因为新的任务可能需要在刚才等待执行任务之前执行,也就是新创建的任务执行时间已经到了,所以要使用notify唤醒执行任务的线程继续进行判断时间是否执行,而且这个原因也是使用wait不使用sleep的原因,如果使用sleep,在新创建任务的执行时间在sleep等待结束时间之前,等待的线程没有办法唤醒,也就不能执行时间到了的任务
【Java多线程】定时器Timer
|
消息中间件 算法 Java
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(下)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(下)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(下)
|
消息中间件 算法 Java
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(上)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(上)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(上)