欧美风格英文网站设计,网页制作作业网站,ui设计与制作培训,制作表情包的软件目录 引言
标准库定时器使用
自己实现定时器的代码
模拟实现的两大方面
核心思路
重点理解 自己实现的定时器代码最终代码版本 引言 定时器用于在 预定的时间间隔之后 执行特定的任务或操作 实例理解#xff1a; 在服务器开发中#xff0c;客户端向服务器发送请求#…目录 引言
标准库定时器使用
自己实现定时器的代码
模拟实现的两大方面
核心思路
重点理解 自己实现的定时器代码最终代码版本 引言 定时器用于在 预定的时间间隔之后 执行特定的任务或操作 实例理解 在服务器开发中客户端向服务器发送请求等待服务器响应但可能因为某一故障导致程序一直无法响应从而容易出现客户端卡死的情况所以为了应对该情况我们通常可以设置一个定时器若未在规定的时间内完成任务则可以做一些操作来取消客户端的等待 标准库定时器使用 import java.util.Timer;
import java.util.TimerTask;public class ThreadDemo24 {public static void main(String[] args) {System.out.println(程序启动);
// 这个 Timer 类就是标准库的定时器Timer timer new Timer();timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(运行定时器任务A);}},3000);timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(运行定时器任务B);}},4000);}
} 上述代码中的 schedule 方法的效果是给定时器 注册一个任务任务不会立即执行而是在指定时间进行执行上述代码中的 schedule 方法有两个参数一个参数为 TimerTask 接口对 run 方法进行重写从而指定要执行的任务另一个参数为等待时间且单位为毫秒 注意 一个定时器可以同时安排多个任务定时器执行完任务之后进程并不会立即退出因为定时器内部需要维护一组线程来执行这些任务这些线程被称为 前台线程当我创建一个定时器并安排任务时定时器会启动一个或多个线程这些线程负责按计划执行任务这些线程会一直运行直到定时器被取消或程序显式地终止这样做到目的是为了确保定时器能够准时执行任务即使主线程已经完成或已退出由于这些前台线程在定时器内部运行所以它们会影响进程的退出如果定时器中的任务尚未完成这些前台线程将阻止进程退出直到所有任务执行完毕或定时器被取消这确保了任务得到完整执行并且程序能够正常结束从而需要注意的是在使用定时器时不再需要它我们应该主动取消定时器以释放资源并停止前台线程的执行这样可以避免不必要的资源占用和线程执行 自己实现定时器的代码 模拟实现的两大方面 在指定时间执行所注册的任务一个定时器可注册多个任务且这多个任务按照约定时间顺序执行 核心思路 有一个扫描线程负责判定任务是否到执行时间需要一个 数据结构 来保存所有被注册的任务 注意 此处的每个任务均带有时间并且一定是时间越靠前就执行所以在当下的场景中使用 优先级队列 便是一个很好的选择时间小的作为优先级高的此时队首元素 就是整个队列中 最先要执行的任务此时 扫描线程仅需扫描一下队首元素即可不必遍历整个队列因为队首元素还没到执行时间后续元素更不可能到执行时间当然 此处的优先级队列会在 多线程 环境下使用因为 调用 schedule 方法是一个线程扫描是另一个线程从而此处涉及到线程安全问题我们可以使用 标准库提供的 PriorityBlockingQueue 阻塞队列本身就是线程安全的所以 带优先级的阻塞队列 便十分切合我们的需求 以下是一个自己实现的定时器 import java.util.concurrent.PriorityBlockingQueue;class MyTask implements ComparableMyTask{
// 要执行的任务内容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();}Overridepublic int compareTo(MyTask o) {
// 当前想要实现队首元素是 时间最小的任务
// 这两是 谁减谁不需要去记往往可以试一试就知道了
// 要么就是 this.time - o.time, 要么就是 o.time - this.timereturn (int) (this.time - o.time);}
}class MyTimer {
// 扫描线程private Thread t null;public MyTimer() {t new Thread(() - {while (true) {
// 取出队首元素检查看看队首元素任务是否到时间了try {synchronized (this) {MyTask myTask queue.take();long curTime System.currentTimeMillis();if(curTime myTask.getTime()) {
// 如果时间还没到就把任务塞回队列queue.put(myTask);
// 在 put 之后进行一个 wait 等待this.wait(myTask.getTime() - curTime);}else {
// 如果时间到了就把任务进行执行myTask.run();}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}// 有一个阻塞优先级队列来保存任务private PriorityBlockingQueueMyTask queue new PriorityBlockingQueue();
// 指定两个参数
// 第一个参数是 任务 内容
// 第二个参数是 任务 在多少毫秒之后执行 如 1000public void schedule(Runnable runnable,long after) {
// 注意这里的时间换算获取当前时间的时间戳加上需要等待的时间就是任务执行的时间MyTask task new MyTask(runnable,System.currentTimeMillis() after);queue.put(task);synchronized (this) {this.notify();}}
}public class ThreadDemo25 {public static void main(String[] args) {MyTimer timer new MyTimer();timer.schedule(new Runnable() {Overridepublic void run() {System.out.println(执行了任务A);}},1000);timer.schedule(new Runnable() {Overridepublic void run() {System.out.println(执行了任务B);}},2000);}
} 运行结果 重点理解 MyTask 类是用来描述一个任务的其中包含 要执行的任务内容 和 任务在什么时候执行 正因为我们使用 优先级阻塞队列 来保存我们所有被注册的任务所以我们需要指定当前任务的优先级是什么此处我们任务的优先级是 越早执行的任务其优先级越高即队首元素是时间最小的任务从而我们需要实现一个 Comparable 接口并重写 compareTo 方法 注意 此处的变量 time 为 long 类型所以需要强制类型转换为 int 类型而且此处到底是this.time - o.time还是o.time - this.time我们仅试试就知道了毕竟就这两种减法不要去死记硬背 关于这里的时间戳 在 Java 中System.currentTimeMillis() 是一个静态方法它返回自1970年1月1日午夜格林威治时间以来当前时间的毫秒数即返回一个 long类型的值表示当前时间与1970年1月1日午夜之间的毫秒数差这个值可以用来计算时间间隔、时间戳等操作类似于输出 1626379152345 这里我们为什么要引入 wait 和 notify 呢 queue.put 操作是从 queue 中取出首元素此时的 queue.put 操作是放在 while 循环中的因为我们想保证任务能够及时执行所以不断的循环取出我们 queue 中的首元素拿出来与当前的时间进行比较以免错过任务的执行时间但是我们会发现当距离队首元素执行任务还有很长一段时间的时候queue 也会快速循环地将队首元素取出、比较、放回那此时很显然就是在 忙等CPU 不停地执行该循环操作却毫无意义那么我们便可以直接引入wait 和 notify 来解决此情形当 queue 取出队首元素进行比较时如果发现其还未到执行时间那么将再次把该元素放回到 queue 中然后再 wait 阻塞等待当前时间与首元素执行时间的时间差正因为在 wait 阻塞等待的期间中可能还会插入新元素并且不能保证该新元素是否会成为 queue 中新的首元素所以在我们每插入一个新元素时都需要进行 notify 一次唤醒线程然后继续执行 while 循环中的操作所以此处不能简单的使用 sleep 进行阻塞等待因为无法感知新元素插入所导致的新改变 使用 wait 的前提是得拥有锁对象所以要进行加锁操作那么此时的 synchronized 有以下两种加锁方式哪种更好呢 我们拿 方案二的加锁方式 进行分析当 线程t 执行 queue.take 语句时此时便会将 queue 的队首元素取出来然后准备进行比较操作假设队首元素的执行时间为 11点且此时的时间为 9点即该队首元素还未到执行时间那么便将会把该元素重新放回到 queue 中如果此时的 线程t 正准备要执行 wait 进行阻塞等待时CPU 转而执行其他也就是在还未 wait 的情况下又新增了一个任务并且此时该任务的执行时间为 10点那么在新增任务的前提下继续执行 wait 阻塞等待 2个小时 此时便会直接错过准备在10点 执行的新任务造成上述情况最主要的原因就是方案二并没有保证 take 和 wait 这两个操作执行的原子性导致在执行这两个操作之间可能会 put 进一个新任务所以我们可以将 synchronized 加锁的范围扩大直接将 锁的范围扩大到 方案一以此想保证 take 和 wait 操作的原子性 但是仅这样我们能解决上述问题嘛也就是能否保证在执行 take 和 wait 这两个操作时执行这两个操作之间不会再 put 进一个新任务显然仅通过扩大上述 synchronized 加锁的范围扩大并不能完全保证我们得保证再对锁对象加锁时其 put 方法也需放入到 同一个锁对象的锁中即将使用方案二 也就是当 扫描线程t 对 锁对象进行了加锁操作此时其他线程便不能调用被 同一个 锁对象 加了锁的代码块具体来说就是当主线程调用 schedule 方法准备执行 queue.put 语句插入新任务时 便因为 扫描线程t 未释放 锁对象所以主线程不能获取到锁对象从而便会阻塞在 锁外从而只要当 扫描线程t 释放了锁对象主线程才能获取到锁对象也就才能执行 queue.put 语句才能往 queue 队列中插入新任务所以通过以上修改我们便能很好的保证在执行 take 和 wait 这两个操作时执行这两个操作之间不会再 put 进一个新任务 仔细思考上述代码我们还会发现问题那就是通过 synchronized 加锁的范围扩大 和 把 put 方法也放入到 同一个锁对象的锁中 这两个操作虽然解决了出现下图所示情况 但是经过上述调整该代码会存在 死锁 的情况假设此时 new 了一个 MyTimer 对象定时器那么此时就会初始化并调用 MyTimer 构造方法构造方法就会创建一个线程t1并开始执行其 run 方法此时 线程t 便会拿到锁对象程序进入 run 方法但是由于 queue 队列中没有元素因此就会在 queue.take 处阻塞等待直到有任务放入队列中此时主线程通过调用 schedule 方法准备往 queue 队列中加入任务但是由于 线程t 已经拿到锁对象了且并未释放锁对象所以此时在准备执行 queue.put 语句时便会阻塞等待所以此时 schedule 无法将任务 put 到 queue 队列中这时 线程t 在阻塞等待schedule 也在阻塞等待就出现了死锁 模拟代码示例 import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;class Syn {private Object locker new Object();BlockingQueueInteger queue new LinkedBlockingQueue();public Syn() {Thread t1 new Thread(() - {synchronized (locker) {try {queue.take();} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();}public void say() throws InterruptedException {synchronized (locker) {queue.put(Integer.valueOf(1));}}
}public class TestSyn {public static void main(String[] args) throws InterruptedException {Syn syn new Syn();Thread.sleep(1);syn.say();System.out.println(主线程打印 queue 中的值 syn.queue.take());}
} 执行结果 我们发现新插入到 queue 中的值并未打印到控制台 通过 jconsole 观察线程情况 综上所述为了 防止死锁的发生我们又需将 queue.put 操作放到 锁外与上文通过 synchronized 加锁的范围扩大 和 把 queue.put 操作也放入到 同一个锁对象的锁中 这两个操作来保证 take 和 wait 这两个操作执行的原子性也就是在执行 take 和 wait 这两个操作之间不会再 put 进一个新任务从而这里 queue.take 无论是放在锁外还是锁内都会引发问题 自己实现的定时器代码最终代码版本 使用 优先级阻塞队列 无论如何修改代码总会存在 问题所以 我们直接转而使用 优先普通级队列不再使用 自带阻塞效果的 take 和 put 方法了 import java.util.PriorityQueue;// 创建一个类, 用来描述定时器中的一个任务
class MyTimerTask implements ComparableMyTimerTask {// 任务啥时候执行. 毫秒级的时间戳.private long time;// 任务具体是啥.private Runnable runnable;public MyTimerTask(Runnable runnable, long delay) {// delay 是一个相对的时间差. 形如 3000 这样的数值.// 构造 time 要根据当前系统时间和 delay 进行构造.time System.currentTimeMillis() delay;this.runnable runnable;}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}Overridepublic int compareTo(MyTimerTask o) {// 认为时间小的, 优先级高. 最终时间最小的元素, 就会放到队首.// 怎么记忆, 这里是谁减去谁?? 不要记!! 记容易记错~~// 随便写一个顺序, 然后实验一下就行了.return (int) (this.time - o.time);// return (int) (o.time - this.time);}
}// 定时器类的本体
class MyNewTimer {// 使用优先级队列, 来保存上述的 N 个任务private PriorityQueueMyTimerTask queue new PriorityQueue();// 用来加锁的对象private Object locker new Object();// 定时器的核心方法, 就是把要执行的任务添加到队列中.public void schedule(Runnable runnable, long delay) {synchronized (locker) {MyTimerTask task new MyTimerTask(runnable, delay);queue.offer(task);// 每次来新的任务, 都唤醒一下之前的扫描线程. 好让扫描线程根据最新的任务情况, 重新规划等待时间.locker.notify();}}// MyTimer 中还需要构造一个 扫描线程, 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,// 就要调用这里的 Runnable 的 Run 方法来完成任务public MyNewTimer() {// 扫描线程Thread t new Thread(() - {while (true) {try {synchronized (locker) {while (queue.isEmpty()) {// 注意, 当前如果队列为空, 此时就不应该去取这里的元素.// 此处使用 wait 等待更合适. 如果使用 continue, 就会使这个线程 while 循环运行的飞快,// 也会陷入一个高频占用 cpu 的状态(忙等).locker.wait();}MyTimerTask task queue.peek();long curTime System.currentTimeMillis();if (curTime task.getTime()) {// 假设当前时间是 14:01, 任务时间是 14:00, 此时就意味着应该要执行这个任务了.// 需要执行任务.queue.poll();task.getRunnable().run();} else {// 让当前扫描线程休眠一下, 按照时间差来进行休眠.// Thread.sleep(task.getTime() - curTime);locker.wait(task.getTime() - curTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}
}// 写一个定时器
public class ThreadDemo27 {public static void main(String[] args) {MyNewTimer timer new MyNewTimer();timer.schedule(new Runnable() {Overridepublic void run() {System.out.println(hello 3);}}, 3000);timer.schedule(new Runnable() {Overridepublic void run() {System.out.println(hello 2);}}, 2000);timer.schedule(new Runnable() {Overridepublic void run() {System.out.println(hello 1);}}, 1000);System.out.println(程序开始运行);}
} 原因如下图所示