多线程|定时器
Java中的定时器是设定一个时间,时间到之后执行指定的代码,定时器的应用场景是非常多的,例如在进行网络通信的时候,设定一个时间,如果执行时间到了对方还没有返回数据,则断开链接并尝试重新链接。
Java库中提供了定时器Timer类,它的核心方法是schedule,其包含两个参数,一个是指定要执行的代码,一个是代码多久后执行的时间。
public static void main(String[] args) { System.out.println("程序启动"); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("程序1启动"); } },3000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("程序2启动"); } },2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("程序3启动"); } },1000); }
上述代码中,程序3设定时间是1000ms,程序2设定的时间是2000ms,程序1设定的时间是3000ms,因此上述代码的执行顺序是程序启动,程序3启动,程序2启动,程序1启动。
实现定时器
定时器的功能是设定一个时间,时间到之后执行指定的代码。由于“任务”是根据时间大小的顺序来执行的,所以考虑使用优先级队列来存储“任务”,时间小的是优先级高的,队首元素就是最先执行的“任务”,用线程去扫描队列的首元素,判断是否到了执行的时间。综上,实现定时器有两个核心:
1、使用优先级队列存储“任务”,队首元素就是最先执行的“任务”;
2、使用一个线程扫描队列的首元素,看是否到达执行时间,时间到了就执行,没到就阻塞等待。
队列中存储的对象的类型是“任务”,“任务”中要包含执行的内容,还有用于判断优先级的时间,我们创建MyTask类,作为“任务”的类型存放在队列中。
class MyTask { private Runnable runnable;//任务 private long time;//多久之后执行任务 public MyTask(Runnable runnable,long time){ this.runnable = runnable; this.time = time; } //获取当前任务的时间,作为后面优先级的比较 public long getTime(){ return time; } //执行任务 public void run(){ runnable.run(); } } class MyTimer { private Thread t = null;//这个线程负责扫描优先级队列中的首元素,判断是否到执行时间 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//保存任务 public MyTimer(){ t = new Thread(){ public void run() { while (true) { try{ //取出首元素,看是否到达执行时间 MyTask myTask = queue.take(); //获取当前时间 long curTime = System.currentTimeMillis(); //看是否到达时间 if(curTime < myTask.getTime()){ //没到时间,将取出的元素放回去 queue.put(myTask); }else { //时间到了,执行任务 myTask.run(); } }catch (InterruptedException e){ e.printStackTrace(); } } } }; t.start(); } public void schedule(Runnable runnable,long after){ MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after); queue.put(myTask); } }
我们将MyTask的属性定义出来之后,将MyTask类型的对象放入到优先级队列中,由一个线程去扫描队列中的元素,这是定时器要做的工作。我们创建MyTimer类来实现定时器的功能。在定时器中我们使用schedule来创建任务,并把任务放到优先级队列中。
public static void main(String[] args) { MyTimer myTimer = new MyTimer(); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("任务1"); } },2000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("任务2"); } },1000); }
使用实现的定时器,看看是否能达到预期的效果,来看看执行结果:
出现上述异常是因为,我们没有定义队列中的元素根据什么来确定优先级。我们这里实现comparable 接口,重写compareTo方法。
上述代码还是存在一个问题,看看出现问题的位置:
如果时间没到,这里会进行重复的拿出,重复的放入的操作,这种现象称为忙等,在这一段时间内,线程始终占着CPU的资源,且做着没意义的工作,为了不让线程忙等,需要线程阻塞式等,即第一次取出元素与当前时间比较,如果时间没到,等待相应的时间间隔,等时间到了之后,进行执行操作,比如任务执行的时间是14点,现在时间是13点,那么等一个小时就好了。根据上述的描述,是否直接使用sleep就行了呢?我们还要考虑一种情况,假如在线程等待的时间内,有新的任务来临,执行的时间是13.30,如果使用sleep的话,那么新任务就无法执行了,这里我们使用wait和notify。使用wait等待,当有新任务来的时候,用notify唤醒wait,再重新计算需要等待的时间。
最终的代码:
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 time; } //执行任务 public void run(){ runnable.run(); } //进行时间的比较 public int compareTo(MyTask o){ return (int) (this.time - o.time); } } class MyTimer { private Thread t = null;//这个线程负责扫描优先级队列中的首元素,判断是否到执行时间 private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//保存任务 public MyTimer(){ t = new Thread(){ public void run() { while (true) { try{ //取出首元素,看是否到达执行时间 MyTask myTask = queue.take(); //获取当前时间 long curTime = System.currentTimeMillis(); //看是否到达时间 if(curTime < myTask.getTime()){ //没到时间,将取出的元素放回去 queue.put(myTask); //这里进行一段时间的等待 synchronized (this){ this.wait(myTask.getTime() - curTime); } }else { //时间到了,执行任务 myTask.run(); } }catch (InterruptedException e){ e.printStackTrace(); } } } }; t.start(); } public void schedule(Runnable runnable,long after){ MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after); queue.put(myTask); //当有新任务进入队列时,唤醒wait synchronized (this){ this.notify(); } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署