定时器: Timer (Java)

导读:本篇文章讲解 定时器: Timer (Java),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、定时器是什么

定时器也是软件开发中的一个重要组件。类似于一个 “闹钟”,即达到一个设定的时间之后,就执行某个指定好的代码。
在这里插入图片描述

定时器是一种实际开发中非常常用的组件。
比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连!
比如一个 Map,希望里面的某个 key 在 3s 之后过期 (自动删除)!
类似于这样的场景就需要用到定时器~~

二、标准库中的定时器

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

public class Demo {
    public static void main(String[] args) {
        // 标准库的定时器.
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到, 快起床!");
            }
        }, 3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到2!");
            }
        }, 4000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到3!");
            }
        }, 5000);

        System.out.println("开始计时!");
    }
}

执行完上述任务之后,进程并没有退出!
因为Timer内部需要一组线程来执行注册的任务,而这里的线程是前台线程,会影响进程退出~~

三、实现定时器

3.1 定时器的构成

  • 一个带优先级的阻塞队列;

为啥要带优先级呢?
因为阻塞队列中的任务都有各自的执行时刻 (delay),最先执行的任务一定是 delay 最小的,使用带优先级的队列就可以高效地把这个 delay 最小的任务找出来~~

  • 队列中的每个元素是一个 Task 对象;
  • Task 中带有一个时间属性,队首元素就是时间最小的 Task;
  • 同时有一个 worker 线程一直扫描队首元素,看队首元素是否需要执行!

3.2 实现细节

1)schedule第一个参数是一个任务,包含两个信息:一个是要执行啥工作;一个是啥时候执行

// 这个类表示一个任务class MyTask
// 要执行的任务
private Runnable runnable;
// 什么时间来执行任务(是一个时间戳) 
private long time;

public MyTask(Runnable runnable,long delay) {
	this.runnable = runnable;
	this.time = System.currentTimeMiLlis() + delay;
}

2)让 MyTimer 能够管理多个任务 (一个Timer是可以安排多个任务的)

前面提及:一个带优先级的阻塞队列最合适!

private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

3)任务已经被安排到优先级阻塞队列中了,接下来就需要从队列中取元素了。
创建一个单独的扫描线程,让这个线程不停的来检查队首元素,看时间是否到了。如果时间到了,则执行该任务!
在这里插入图片描述
4)优先级阻塞队列需要进行元素比较,所以要实现Comparable接口!

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }

5)还存在严重的问题!!!
在这里插入图片描述
可以使用wait来阻塞线程,当schedule加入新任务时再唤醒!:

Thread t = new Thread(() -> {
	while (true){
		try {
			//取出队首元素
			MyTask task = queue.take();
			//假设当前时间是2:30,任务设定的时间是2:30,显然就要执行任务了
			//假设当前时间是2:30,任务设定的时间是2:29,也是到点了,也要执行任务long curTime = System.currentTimeMillis();
			if (curTime >= task.getTime()){
				//到点了,改执行任务了!!
				task.getRunnable(). run();
			} else {   
				//还没到点
				queue.put(task);
				//没到点,就等待
				synchronized (locker){
					locker.wait( timeout: task.getTime() - curTime);
				}
		    }
		}catch (InterruptedException e) {
				e.printstackTrace();
			}
		}
	});

public void schedule(Runnable runnable, long after) throws InterruptedException {
    MyTask myTask = new MyTask(runnable, after);
    queue.put(myTask);
    synchronized (locker) {
        locker.notify();
    }
}

这时的代码还有一个问题!!!
假设扫描线程先执行,执行take之后线程切换到schedule线程。
schedule线程新增一个任务,这个任务1:00 执行。schedule执行完毕之后,执行notify ( t线程刚执行完take,还没wait呢~ 这个notify相当于空打了一炮:虽然通知了,但是没有唤醒任何线程),然后回到扫描线程继续往下执行,然后发现当前时刻是12:00,任务时间是2:30?!这时就把任务塞回队列,然后就进行wait,wait时间是2.5小时!
这就意味着,刚才新来的这个1:00要执行的任务,就被错过了!!!

多线程的执行过程是非常复杂的,任何两行代码之间都可能出现线程切换,甚至一行代码中就可能会切换多次~~
写代码的时候脑子里就得演绎出各种各样的情况!!!

因此需要把锁的范围放大

public MyTimer() {
    // 创建一个扫描线程.
    Thread t = new Thread(() -> {
        while (true) {
            try {
                synchronized (locker) {
                    // 取出队首元素
                    MyTask task = queue.take();
                    // 假设当前时间是 2:30, 任务设定的时间是 2:30, 显然就要执行任务了.
                    // 假设当前时间是 2:30, 任务设定的时间是 2:29, 也是到点了, 也要执行任务.
                    long curTime = System.currentTimeMillis();
                    if (curTime >= task.getTime()) {
                        // 到点了, 改执行任务了!!
                        task.getRunnable().run();
                    } else {
                        // 还没到点
                        queue.put(task);
                        // 没到点, 就等待
                        locker.wait(task.getTime() - curTime);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}

刚才出现问题的原因就是notify在take和wait之间执行的。
现在把扫描线程中的锁范围放大了,此时就可以避免notify在take和wait之间执行了!扫描线程会先拿到锁,然后take,然后中间逻辑,一直到wait;在这个过程中,schedule线程会阻塞等待锁。直到扫描线程执行了wait后,扫描线程释放了锁,schedule线程就拿到了锁,进行了通知,这个时候wait就被立即唤醒了!接下来再次重新取队首元素,就把1:00执行的任务取出来了~~

如果把schedule方法里的锁范围也扩大可以吗?:

public void schedule(Runnable runnable, long after) throws InterruptedException {
   	synchronized (locker) {
    	MyTask myTask = new MyTask(runnable, after);
    	queue.put(myTask);
        locker.notify();
    }
}

运行代码后,我们发现两个线程都会进入阻塞状态,即死锁!!!为什么呢?

如果代码死锁了,一定要先拿jconsole看下线程的调用栈,明确死锁是卡死在哪行代码!
通过jconsole,我们找到死锁位置:
在这里插入图片描述
原因:假设此处是先执行42行这里的代码:先加锁,然后尝试从队列里take取队首元素。而queue是一个阻塞队列,特点就是队列为空时取值则阻塞!!!
此时扫描线程就阻塞在45行了,什么时候会解除阻塞?得有线程往队列里加元素!
主线程69行要通过schedule往里加元素,但是加元素的前提是先加锁,但是此时这个锁是被43行代码扫描线程占用着呢,schedule获取不到锁,无法执行put!!!
这不就死锁了吗?!~~

阻塞队列take操作wait的时候是释放队列内部的锁对象,这个代码中还有一个自己定义的locker对象。
两个线程两把锁~~

3.3 完整代码

import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

// 这个类表示一个任务
class MyTask implements Comparable<MyTask> {
    // 要执行的任务
    private Runnable runnable;
    // 什么时间来执行任务. (是一个时间戳)
    private long time;

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}

class MyTimer {
    private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    private Object locker = new Object();

    public MyTimer() {
        // 创建一个扫描线程.
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        // 取出队首元素
                        MyTask task = queue.take();
                        // 假设当前时间是 2:30, 任务设定的时间是 2:30, 显然就要执行任务了.
                        // 假设当前时间是 2:30, 任务设定的时间是 2:29, 也是到点了, 也要执行任务.
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 到点了, 改执行任务了!!
                            task.getRunnable().run();
                        } else {
                            // 还没到点
                            queue.put(task);
                            // 没到点, 就等待
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long after) throws InterruptedException {
        MyTask myTask = new MyTask(runnable, after);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
}

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到1!");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到2!");
            }
        }, 4000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("时间到3!");
            }
        }, 5000);
        System.out.println("开始计时");

        ArrayDeque<String> a = new ArrayDeque<>();
        a.peekLast();
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/118582.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!