多线程之定时器 - 详解


前言

本篇了解定时器的方法,参数,并模拟实现定时器,进一步对定时器有了更深的理解,如有错误,请在评论区指正,让我们一起交流,共同进步!



本文开始

1. 认识定时器

1.什么是定时器?
定时器就是设置一个时间,等待时间到了,就执行已经写好的代码;

2.定时器的类
Java标准库提供的定时器:Timer 它是java.util提供的一个类;
定时器的核心方法:schedule();
它有两个参数:第一个是实现要执行的任务代码,第二个是多长时间之后执行(毫秒级别);

定时器代码实现:

import java.util.Timer;
import java.util.TimerTask;

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

【注】这里的new TimerTask() 接口实现了Runnable 方法;

2. 模拟实现定时器

2.1 创建MyTimer类

定时器主要的类就是Timer,这里就得自己实现!
实现这个类就需要用到带有优先级的阻塞队列,为什么呢?
定时器内部有很多任务需要管理,多个线程可能都在执行schedule方法,但是它们的执行时间是不同的,就需要让他们按照顺序执行,并保证线程是安全的,这就用到了带有优先级的阻塞队列;

在多线程条件下,为了保证线程是安全的,此时Java标准库中有优先级阻塞队列就满足这些条件;
使用优先级阻塞队列: PriorityBlockingQueue

但是问题又来了,优先级阻塞队列里面放置什么元素呢?
优先级队列中存放的元素是什么?
因为放置的元素不是单一的,需要创建一个类,封装一下①执行的任务是什么(Runnable);②任务什么时候执行(time)

2.2 创建优先级队列中放置的元素MyTask

1.描述执行的任务
public Runnable runnable;
2.描述要执行任务的时间点
public long time;
这里为了方便后续判定,使用绝对的时间戳;
取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳;
3.构造方法为下面初始化参数做准备
构造public MyTask()

2.3 在MyTimer类写核心方法schedule

1.参数
①Runnable runnable - 执行的任务;② long delay - 执行任务要等待的时间;
2.根据参数构造创建对象MyTask, 并把对象插入到阻塞队列中

    public void schedule(Runnable runnable, long delay) {
        //创建任务,并放入阻塞队列中, 此时也应该在MyTask任务中写带有两个构造参数的构造方法
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
    }

2.4 构造线程,执行具体任务

1.创建无参 public MyTimer() {…}
2.创建线程 t
3.使用循环,每次取出阻塞队列中的元素(任务),获取当前任务的当前时间,判断任务执行时间与当前时间的大小;

    public MyTimer() {
        Thread t = new Thread( () -> {
           //使用循环不断取出阻塞队列中的元素,比较是否到任务执行的时间
           while (true) {
               try {
                   //取出阻塞队列中的元素
                   MyTask myTask = queue.take();
                   //获取当前任务的时间
                   long curTime = System.currentTimeMillis();
                   //比较时间到了就执行任务,时间没有到就放回到阻塞队列中
                   if(myTask.time < curTime) {
                       //执行任务
                       myTask.runnable.run(); 
                   }else {
                       //放回阻塞队列中
                       queue.put(myTask);
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }

基本的定时器框架已经完成,但是并不能执行,思考一下,这个定时器存在什么问题呢???
从代码中分析一下吧!定时器不完全代码:

class MyTask {
    public Runnable runnable;//描述要执行的任务
    public long time;//描述任务要执行的时间 =》 这里采取绝对时间
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //当前时间 + dealy = 执行任务的时间
        this.time = System.currentTimeMillis() + delay;
    }
}
class MyTimer {
    //构造优先级阻塞队列
    PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    //构造schedule方法,delay=> 延迟
    public void schedule(Runnable runnable, long delay) {
        //创建任务,并放入阻塞队列中, 此时也应该在MyTask任务中写带有两个构造参数的构造方法
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
    }
    //创建线程
    public MyTimer() {
        Thread t = new Thread( () -> {
           //使用循环不断取出阻塞队列中的元素,比较是否到任务执行的时间
           while (true) {
               try {
                   //取出阻塞队列中的元素
                   MyTask myTask = queue.take();
                   //获取当前任务的时间
                   long curTime = System.currentTimeMillis();
                   //比较时间到了就执行任务,时间没有到就放回到阻塞队列中
                   if(myTask.time < curTime) {
                       //执行任务
                       myTask.runnable.run(); 
                   }else {
                       //放回阻塞队列中
                       queue.put(myTask);
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }

}

来看一下存在的问题!!!

2.5 出现的两种问题

1.优先级队列的比较规则
产生原因:在优先级阻塞队列中,每个任务执行的时间是不同的,为了保证有序执行,使用优先级阻塞队列,但是比较两个任务的执行时间的规则没有定,无法正常排序,所以需要实现自定义的比较规则;
解决方式:实现Comparable方法,重写comparTo()
代码实现:

class MyTask implements Comparable<MyTask>{
    public Runnable runnable;//描述要执行的任务
    public long time;//描述任务要执行的时间 =》 这里采取绝对时间
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳
        //currentTimeMillis()=> 时间戳:当前时刻和基准时刻的ms数之差
        this.time = System.currentTimeMillis() + delay;
    }

    @Override
    public int compareTo(MyTask o) {
        //获取每次取出最小的时间
        return (int)(time - o.time);
    }
}

2.忙等
产生原因:假设一个任务执行的时间为12点,当前时间是11点30;在这期间,一直读取当前时间,cup一直处于被暂用的状态,会有资源的浪费 =》这就是忙等
解决方式:当没有到达任务要执行的时间,使用wait()方法等待,等时间到了再执行;
【注】wait() 需要配合加锁一起操作,就需要创建新的锁对象;
代码实现:在MyTimer中创建锁对象,调用wait()方法,这里简化跟上述重复的代码就不写了,只写增加的代码的部分;

//在MyTimer内
  	//使用wait方法,就需要加锁,加锁就需要锁对象,创建锁对象
    private Object locker = new Object();
    
        //创建线程
    public MyTimer() {
        Thread t = new Thread( () -> {
           while (true) {
               try {
                   synchronized (locker) {
                       //取出阻塞队列中的元素
                       MyTask myTask = queue.take();
                       //获取当前任务的时间
                       long curTime = System.currentTimeMillis();
                       if(myTask.time < curTime) {
                           //执行任务
                           myTask.runnable.run(); //???
                       }else {
                           //时间没到,放回阻塞队列中
                           queue.put(myTask);
                           //防止忙等,使用wait等待时间的差值
                           locker.wait(myTask.time - curTime);
                       }
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }

加锁wait()后,发现如果出现新的任务怎么办呢?
解决方式:在插入阻塞队列时,唤醒wait(), 可以使用notify()
代码实现:

    //构造schedule方法,delay=> 延迟
    public void schedule(Runnable runnable, long delay) {
        //创建任务,并放入阻塞队列中, 此时也应该在MyTask任务中写带有两个构造参数的构造方法
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
        //在加入新任务的时候,就唤醒
        synchronized (locker) {
            locker.notify();
        }
    }

2.6 测试代码

测试思想描述:
创建定时器,多个线程调用schdule方法,规定执行的任务和经过多次时间执行这个任务;

主函数测试代码:

    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("run2");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("run1");
            }
        },1000);
        System.out.println("main");
    }

执行结果:

在这里插入图片描述

3. 定时器总代码

代码如下:

import sun.applet.Main;

import java.util.concurrent.PriorityBlockingQueue;
//创建一个类封装一些,用来表示一个任务
class MyTask implements Comparable<MyTask>{
    //描述执行的任务
    public Runnable runnable;
    //为了方便后续判定,使用绝对时间戳
    public long time;//描述这个任务要执行的时间点 =》 任务要执行的绝对时间

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        //取当前时刻的时间戳 + delay, 作为该任务实际执行的时间戳
        //currentTimeMillis()=> 时间戳:当前时刻和基准时刻的ms数之差
        this.time = System.currentTimeMillis() + delay;
    }

    //阻塞队列的比较规则
    @Override
    public int compareTo(MyTask o) {
        //每次取出时间最小的元素
        return (int)(time - o.time);
    }
}
class MyTimer {
    //使用带有优先级的阻塞队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    //①优先级的比较规则:阻塞队列中MyTask元素的比较,需要设置一个比较规则

    //创建锁对象,进行加锁操作
    private Object locker = new Object();

    //定时器中的schedule方法
    //delay: 是一个像2000这样的数字(表示怎么长时间后,执行该任务)
    public void schedule(Runnable runnable, long delay) {
        //根据参数,构造MyTask, 插入阻塞对列
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
        //新的任务来了:插入元素的时候,唤醒等待
        synchronized (locker) {
            locker.notify();
        }
    }

    //构造线程,负责执行具体的任务
    public MyTimer() {
        //创建线程
        Thread t = new Thread( () -> {
            //②忙等:循环true会一直访问(相当于一直看时间到了执行时间没有)这样会一直占用cpu =》这就是忙等
            //解决方法:等待过程需要释放cpu,使用wait操作,可以随时提前结束
           while (true) {
               try {
                   //了解阻塞队列,只有则是队列的入队列和出队列,没有阻塞队列的查看对首元素
                   // =》只能出队列后才能看到该任务并判断到执行时间了没有,没有会塞会阻塞队列中
                   synchronized (locker) {
                       //取出阻塞队列中的元素
                       MyTask myTask = queue.take();
                       //获取当前任务的时间
                       long curTime = System.currentTimeMillis();
                       if(myTask.time <= curTime) {
                           //时间到了,执行任务;=》执行时间小于当前时间
                           myTask.runnable.run();  //???
                       }else {
                           //执行任务的时间还没到,
                           //把刚才取出的任务,重新塞会阻塞队列中
                           queue.put(myTask);
                           locker.wait(myTask.time - curTime);
                           //等待过程中,新任务来了为什么要中间唤醒原来的等待???
                           //因为可能会遇到下一个任务的时间比等待的时间要短的任务
                           // 此时唤醒等待,在取任务元素的时候,就会取到时间短的任务 =》 任务有优先级的排列
                           
                       }
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("run3");
            }
        },3000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("run2");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("run1");
            }
        },1000);
        System.out.println("main");
    }
}


总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值