多线程案例-实现定时器

1.定时器是什么

定时器是软件开发中的一个重要组件,功能是当达到一个特定的时间后,就执行某个指定好的代码

定时器是一个非常常用的组件,特别是在网络编程中,当出现了"连接不上,卡了"的情况,就使用定时器做一些操作来止损

标准库中也提供了定时器

标准库中的Timer类

标准库提供了一个Timer类,Timer类的核心方法为schedule(安排,预定;将……列入计划表或清单)

schedule包含两个参数
第一个参数指定即将要执行的任务代码
第二个参数指定多多长时间之后执行(单位ms)

下面是用一下定时器

可以看到有两个参数

TimerTask类就是一个实现了Runnable接口的类,来描述指定的任务

delay是指定的时间后执行任务!

运行程序,经过指定的时间后,执行了run()中的语句

2.实现定时器

定时器的核心

注册任务后需要保证任务在指定的时间要被执行
单独在定时器内部,创建一个线程,让这个线程周期性的扫描,判定任务是否到时间了,如果到时间了就执行,没到就继续等待
一个定时器能连续注册N个任务,N个任务是按照最初约定的时间按顺序执行
这N个任务肯定需要一种数据结构来保存,不难发现,我们可以使用优先级队列,我们每个任务都是带有时间的,按照时间小的作为优先级高的,此时队首元素一定是最先要执行的任务,这时候扫描线程也只需要扫描队首元素即可,不必扫描整个队.如果队首元素没有到执行时间,那么其它元素也不可能到达执行时间!!

简而言之,定时器的核心:

1.有一个扫描线程,判断是否到执行时间.

2.还得有一个数据结构保存被注册的任务.

此处优先级队列是在多线程环境下使用的,因此要关注线程安全问题!自己手动加锁,或者使用标准库提供的PriorityBlockingQueue,它既有优先级又符合线程安全的要求

实现代码

我们先创建一个任务类

class MyTask{
    //任务内容
    private Runnable runnable;
    //任务指定的时间(ms时间戳表示)
    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();
    }

队列里的"任务"使用Runnable表示,描述的是任务的内容

使用时间戳描述任务什么时候被执行

然后创建一个定时器

class MyTimer{
    //扫描线程
    private Thread t = null;
    //阻塞优先级队列来保存任务
    private PriorityBlockingQueue<MyTask> queue =
            new PriorityBlockingQueue<>();
}

我们要给定时器类提供一个"schedule"方法来注册任务

//指定两个参数,一个是任务内容,一个是多长时间后执行任务
    public void schedule(Runnable runnable,long after){
        //注意时间的换算
        MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(myTask);
    }

接下来要实现一个比较麻烦的操作,就是扫描线程的实现

public MyTimer(){
        t = new Thread(()->{
           while (true){
               try {
                   //取出队首元素,检查是否到执行时间了
                   //如果到了,就执行
                   //如果没到,就放回队列
                   //如果没有元素证明没有任务,会阻塞等待
                   MyTask myTask = queue.take();
                   long curTime = System.currentTimeMillis();
                   if(curTime < myTask.getTime()){
                       //没到点,不用执行
                       //到点了,开始执行
                       queue.put(myTask);
                   }else{
                       myTask.run();
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
    t.start();
    }

上述代码大致实现了扫描线程的功能,但是还存在两个问题

第一个问题,我们还要明确我们的任务优先级是怎样的,还没指定

此时我们如果测试:

public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }

因为两个任务的优先级关系还没用comparable来设置,或者单独实现一个比较器comparator

运行程序

第二个问题,如果队首任务是四点进行执行,在两点的时候,线程开始扫描,就会一直从队首取出检查,发现没到执行时间,又放回去,反反复复!!直到四点开始执行.我们使用优先级队列来存储的,放回元素,堆就会进行一次调整,将这个任务又调整至队首,下次取出,还是这个元素!

这个循环没有阻塞,会快速的进行循环,是没有意义的,占用了cpu资源.这种情况称为"忙等",我们要对代码进行调整,进行阻塞式等待,sleep,wait..

如果等待时间明确,我们使用sleep可行吗?

此处看似等待时间是明确的,但是我们可能任意时间会来一个新的任务调用schedule,注册任务,那么队首元素就换了,必须得扫描出来.

如果用sleep,那么在sleep过程中就可能注册新的任务,如果在队首元素执行的时间前就要执行新注册的任务,然而用的sleep,就会错过这个任务的执行了
因此使用wait()notify()更合适,使用wait()进行等待,如果有新任务调用schedule,就notify(),重新检查一次,计算等待的时间
并且,wait()还有个超时时间的版本,如果没有新任务,则最多等到队首元素的执行时间就自动唤醒了

这样改动之后,我们既不会一直重复无用操作,也不会错过执行新注册任务

线程安全问题

代码到这里,还有个线程安全问题

我们考虑一个极端情况

如果代码执行到wait之前,这个线程被调度走了,当线程又被调度执行时,接下来就要进行wait操作,它的wait时间是算好了的,比如curTime是13:00,getTime是14:00,即将会wait一个小时,但是还没执行wait.

在该线程被调度走的过程中,如果另一个线程调用了schedule,注册了一个13:30执行的任务,此时schedule会执行notify()将wait()唤醒,但是扫描线程的wait()还没有执行呢,所以notify并没有实际作用,虽然新任务插入到队列中了,也是在队首.但是这个线程紧接又执行wait()一个小时,错过了这次任务的执行时间13:30

这都是多线程随机调度产生的,take和wait操作并非是原子的,如果这个过程是原子的,给它加上锁,保证不会有新的任务过来,就解决问题了,换言之就是要保证每次notify时,确实都在wait!

我们将锁的粒度变大,保证take和wait操作是原子的,就不会出现线程安全问题了

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YoLo♪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值