网络广告网站怎么做泉州seo
目录
编辑
定时器
定时器的使用
三.定时器的实现 MyTimer
3.1 分析思路
1. 创建执行任务的类。
2. 管理任务
3. 执行任务
3.2 线程安全问题
定时器
定时器是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.
定时器的使用
标准库中的定时器 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule . schedule 包含两个参数.
第一个参数是继承timetask抽象类的类实例且内部重写了run方法(这里的匿名类隐式继承了TimerTask):指定即将要执行的任务代码(timetask实现了runable接口所以有run方法)
第二个参数指定多长时间之后执行 (单位为毫秒).
Timer timer = new Timer(); timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");} }, 3000);
执行schedule方法的时候,系统把要执行的任务放到timer对象中,与此同时timer对象里头自带一个线程叫做“扫描线程”,一旦时间到扫描线程就会执行刚才安排的任务,执行完所有任务后线程也不会销毁,会阻塞等待直到其他的任务被放到timer对象中再继续执行(就这么重复
三.定时器的实现 MyTimer
3.1 分析思路
对于定时器来说:
- 创建描述一个要执行的任务(任务内容 + 执行任务时间)的类
- 管理多个任务,通过一定的数据结构,把多个任务存起来
- 有专门的线程,执行这里的任务
1. 创建执行任务的类。
我们在调用 schedule 时,传的是延迟时间 “delay” 值。但是,描述任务时,不太建议使用 delay
表示,最好使用 “绝对时间”(时间戳)来表示~~
public class MyTimerTask implements Comparable<MyTimerTask>{//此处这里的 time,通过毫秒时间戳,表示这个任务具体啥时候执行private long time;private Runnable runnable;public MyTimerTask(Runnable runnable,long delay){this.time = System.currentTimeMillis() + delay;this.runnable = runnable;}public void run(){runnable.run();}public long getTime(){return time;}@Overridepublic int compareTo(MyTimerTask o) {//比如,当前时间是 10:30,任务时间是 12:00,不应该执行//如果当前时间是 10:30,任务时间是 10:29,应该执行//谁减去谁,可以通过实验判断return (int) (this.time - o.time);} }
2. 管理任务
使用 List 管理任务,不是一个好选择——因为后续执行列表中的任务时,就需要依次遍历每个元素;执行完毕后,还需要把对应的任务从 List 中删除掉。
我们需要按照时间来执行这里的任务。只要能够确定所有任务中,时间最早的任务,判定它是否到该执行的时间即可。如果时间最早的任务还没到执行时间,其他任务更不可能到时间了。因此,我们使用堆数据结构(涉及到队列中的元素排序时,考虑堆)——PriorityQueue<MyTimerTask>(优先级队列管理元素时,需要有比较方法,才能排序存储。因此,在实现 MyTimerTask 类时,要继承 Comparable<MyTimerTask> 接口,重写 compareTo比较方法)
3. 执行任务
当创建 MyTimer 对象,调用无参构造方法时,便创建一个线程,循环执行从队列中取出任务的操作:取出队列中 “绝对时间” 最早的任务——如果当前时间 >= 此任务的时间(已经到达此任务的执行时间),便可调用run方法执行,执行完毕后从队列中删除; 如果当前时间 < 此任务的时间(没到此任务的执行时间),则继续执行循环。(所以,在实现 MyTimerTask 类时,要有 run方法 和 getTime方法)
除此之外 ,还需要有 schedule 方法添加任务。
import java.util.PriorityQueue;public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();public MyTimer() {Thread t = new Thread(() -> {while (true){if(queue.isEmpty()){ continue;}MyTimerTask task = queue.peek();//判断是否满足执行条件if (System.currentTimeMillis() >= task.getTime()) {task.run();//执行完后,便从队列中删除queue.poll();}else{continue;}}});t.start();}public void schedule(Runnable runnable, long delay) {MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);}
}
3.2 线程安全问题
当前这个代码,是没有考虑线程安全问题的。
PriorityQueue 这个类自身,是非线程安全的,并且又是多个线程来进行操作,一定存在线程安全问题的风险。因此,要在涉及队列相关操作的地方加锁。(让删的时候不能进行加入操作,加的时候不进行删除操作)
import java.util.PriorityQueue;public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private final Object locker = new Object();public MyTimer() {Thread t = new Thread(() -> {while (true){synchronized(locker){if(queue.isEmpty()){ continue;}MyTimerTask task = queue.peek();if (System.currentTimeMillis() >= task.getTime()) {task.run();queue.poll();}else{continue;}}}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized(locker){MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);}}
}
但是,加完锁以后,又出现了线程安全问题。
1)初始情况下,如果队列中,没有任何元素。此时,就会在短时间内执行大量循环,这样的执行是没有意义的,导致“线程饿死”。
因此,我们需要添加 wait 和 notify 机制:队列为空时,进行等待;添加任务时,就唤醒线程。
2)假设队列中,已经包含元素了,并且当前时间是 10:45,任务时间 12:00(类似于,我定了12:00的闹钟,现在是 10:45)。在判断任务是否满足执行条件时,不满足就会一直循环(相当于每隔一会儿就看一眼闹钟),这样无意义的执行就一直占用着cpu资源,导致 “线程饿死”。
因此,我们需要添加一个有等待期限的 wait(等待 1h15min 就会执行),当到达任务执行时间,wait 就结束了。如果在等待过程中,又再次调用 schedule 方法,也会唤醒这里的 wait,进行新一轮的判断。
线程安全版:
import java.util.PriorityQueue;public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private final Object locker = new Object();public MyTimer() {Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {while (queue.isEmpty()) {//如果还没添加任务,会不断循环执行判断,出现线程饿死。//continue;//因此,使用wait等待,当添加任务后唤醒locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() >= task.getTime()) {task.run();queue.poll();} else {//如果还没到任务执行时间,依旧不断循环判断,出现线程饿死。//continue;//因此,使用有等待期限的 wait,计算执行的时间与当前时间的差值//当添加新的任务后,wait 被唤醒,再进行新的判断locker.wait(task.getTime() - System.currentTimeMillis());}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized (locker){MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);// 唤醒 waitlocker.notify();}}
}
* 能否将第二处的 wait 改为 sleep 呢? ——不能!!
不应该使用sleep,可能存在以下情况:
1)在 sleep 阻塞1h15min 的过程中,新来了一个时间更早的任务,比如 11:30 要执行。如果使用 wait ,每次新来的任务,都会把 wait 唤醒,重新设定 wait 的等待时间。而 sleep 不会被唤醒,依旧在阻塞着.....
2)sleep 休眠的时候,不会释放锁。因此,在休眠的时候就是“抱着锁”,其他人想拿锁就拿不到了。也就是说你休眠的时候,就不能进行增加线程
* PriorityQueue 是线程不安全的类,能否使用 PriorityBlockingQueue 线程安全的阻塞队列呢?——不能!!
如果使用线程安全的队列,会导致代码中从 一把锁 变成 两把锁,很容易出现死锁的情况,比如持有和请求的情况(并非100%一定出现,但是需要程序员精心控制加锁顺序,使得编写代码的复杂度提高了。如果通篇代码 只有一把锁,就能更容易地解决问题)