JavaEE(系列10) -- 多线程案例3(定时器)

目录

1. 定时器

2. 标准库中的定时器

3. 实现定时器

3.1 创建带优先级的阻塞队列 

3.2 创建MyTask类

3.3 构建schedule方法

3.4 构建timer类中的线程

3.5 思考



1. 定时器

定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.

2. 标准库中的定时器

  1. 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  2. schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

使用步骤:

1. 实例化Timer对象

2.调用timer.schedule("任务",执行时间) 

public class timerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello4");
            }
        },4000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello3");
            }
        },3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello2");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello1");
            }
        },1000);
        System.out.println("hello");
    }
}

 运行结果:先打印主进程的hello,后面陆续按照指定的时间进行打印每个线程的内容

3. 实现定时器

定时器的构成

  1. 一个带优先级的阻塞队列 
  2. 队列中的每个元素都是一个Task对象
  3. Task 中带有一个时间属性, 队首元素就是即将执行的任务
  4. 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行

为什么使用带有优先级的堵塞队列?

答案:因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.

3.1 创建带优先级的阻塞队列 

3.2 创建MyTask类

队列中的每个元素都是一个Task对象,创建Mytask类,用来描述要执行的任务,以及执行的时间.(用于传入堵塞队列)

此处需要注意:

我们需要将Mytask类实现Comparable接口,根据执行的时间进行比较. 这样才能传入带有优先级的堵塞队列.

 最终MyTask类代码为:

class MyTask implements Comparable<MyTask>{
    public Runnable runnable;
    public long time;

    public MyTask(Runnable runnable, long delay){
        // 取当前时刻的时间戳 + delay = 当前该任务实际执行的时间戳
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }


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

3.3 构建schedule方法

通过schedule方法往队列中插入Task对象

3.4 构建timer类中的线程

Timer 类中存在一个 worke 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务.

此线程的实现思路

  • 1. 线程要执行在队列中不断地取出任务  queue.take();
  • 2.取出任务要进行比较当前系统时间与任务执行时间
  • 3.如果任务执行时间小于当前系统时间,就说明要执行任务了.调用 myTask.runnable.run();
  • 4.如果当前取出的任务执行时间大于当前系统时间,就说明任务还没有到执行时间,将任务推送到队列中.同时进入堵塞等待
  •  5. 在schedule方法中,往优先级队列推送任务之后,同时加一个notify方法,用来唤醒此时正在堵塞的线程,使得堵塞等待解除,重新取队首任务进行比较时间.

 加wait notify的好处,就是work线程不需要一直进行取队首元素,这样会消耗系统资源,造成没必要的浪费,只需要等待堵塞当前距离执行任务的时间差就可以,当有新的任务添加进来的时候接触堵塞,重新进行计算时间差,再决定是否进行执行任务,还是进入堵塞状态.

public class MyTimer {
    private final PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    // 创建一个锁对象
    private final Object locker = new Object();
    public MyTimer(){
        //1.创建一个线程
        Thread work = new Thread(()->{
           while (true){
               try {
                   synchronized (locker) {
                       //2.队列中取出一个任务
                       MyTask myTask = queue.take();
                       //3 获取当前时间
                       long curTime = System.currentTimeMillis();
                       //4. 任务执行时间与当前时间进行对比
                       if (myTask.time <=  curTime){
                           //4.1 任务执行时间小于等与当前时间,说明应该要执行任务了
                           myTask.runnable.run();
                       }else {
                           //4.1 任务执行时间大于等与当前时间,说明该任务还没有到执行的时间,再将刚才取出的任务放回原来的队列
                           queue.put(myTask);
                           locker.wait(myTask.time-curTime);

                           //针对这个wait():
                           //1.方便随时唤醒,比如当前时刻是14:00,约定14:30要执行上课任务,
                           //此时取出队首元素,发现时间没有到,就wait(任务执行时间-当前时间)
                           //2.当新的任务来了,需要比之前的队伍提前执行,那么就需要进行唤醒之前的wait(),
                           //重新取队首元素,进行比较时间,确定wait()的时间.

                       }
                   }
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        work.start();
    }

    public void schedule(Runnable runnable, long delay){
        // 根据参数,构造MyTask,插入队列
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
        synchronized (locker){
            locker.notify();
        }
    }
}

3.5 思考

我们将锁加在了整个执行任务.此时我们如果只针对wait进行加锁?这样线程安全吗?不安全的话给出理由.

 

答案:会出现线程不安全的情况

比如下图解释:

        我们此时有两个线程,T1线程此时取出一个任务(执行时间为14:30),比较当前时间(14:00),还没有到执行时间,此时将任务推送给队列,但是在推送之前,此时有一个T2线程,正在插入一个新的任务(14:10),同时执行了notify操作,但是此时T1线程并没有wait,此时就空打一炮,此时T1线程开始拿到锁,进行堵塞等待,但是此时等待的时间为(14:30 - 14:00),T2线程插入的新任务还有10分钟需要执行,但是因为之前已经notify一次,此时堵塞的时间无法进行唤醒操作,所以T2线程插入的这个任务要等到14:30才能执行,这就引起线程的不安全.

 

        当我们加锁在整个(取出任务和推送任务),T1线程一定在wait之前不会使得T2线程执行notify操作,因为T1线程在加锁中,等待wait之后才会解除锁,等待T1的锁解除,T2才会执行notify操作. 进而去唤醒T1线程中的wait.

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

哈士奇的奥利奥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值