多线程|定时器

发布时间 2023-09-05 18:14:30作者: 司丝思

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();
        }
    }
}