多线程(初阶八:计时器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)如果没到时间,就要等待到时在执行要执行的代码

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


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

相关文章
|
2月前
|
Java
【Java多线程】定时器Timer
【Java多线程】定时器Timer
37 0
【Java多线程】定时器Timer
|
9月前
|
druid 关系型数据库 MySQL
高并发下 MySQL Statement Cancellation Timer 的线程数暴涨
高并发下 MySQL Statement Cancellation Timer 的线程数暴涨
|
12月前
|
Java
【JavaEE】多线程之定时器(Timer)
【JavaEE】多线程之定时器(Timer)
|
11月前
|
安全 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、延时线程池?(上)
|
安全 Java
Java线程之join方法&死锁&Timer
线程锁 锁定的是对象 1.放置在方法的结构上 public synchronized void test(){ 好多代码 执行代码 好多代码 } 对象.test(); 对象被某一个访问他的线程锁定 2.放置在方法(构造 块)的内部(看起来是包含着一堆代码) public void test(){ 好多代码 synchronized(对象){ 执行代码 } 好多代码 } 3.线程相关的一些方法 sleep(); run(); start(); setPriority();--------
181 0