1、定时器是什么?
1.1、概念
定时器是软件开发中一个重要的组件,类似于一个”闹钟“,达到一个设定的时间之后,就会执行某个指定的代码。
定时器是一种实际开发中非常常用的软件.
比如网络通信中,如果对方500ms没有返回数据,则断开连接尝试重连.
比如一个Map,希望里面的某个key在3s之后过期(自动删除).
类似以上场景就需要定时器.
1.2、标准库中的定时器
*标准库中提供了一个Timer类,Timer类的核心方法为schedule.
*schedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间后执行(单位为毫秒).
/** * JDK中的计时器 */ public class Exe_01 { public static void main(String[] args) { //定义一个计时器 Timer timer=new Timer(); //往定时器里添加任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println("全名星制作人们大家好,我是练习时长两年半的个人练习生蔡徐坤"); } }, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("喜欢唱、跳、rap、篮球"); } }, 4000); timer.schedule(new TimerTask(){ @Override public void run(){ System.out.println("music!"); } }, 6000); } }
1.3自定义实现定时器
1、定时器中需要有一种数据结构来组织任务执行,可以考虑一个带优先级的阻塞队列,这是因为阻塞队列中的任务都有各自的执行时刻(delay),最先执行的任务一定是的delay最小的,使用带优先级的队列就可以高效的把这个delay最小的找出来。
2、需要单独定义一个类来描述具体的任务业务和执行时间。
3、需要一个专门线程来扫描数据结构中的任务到没有到执行时间。
4、需要一个方法去添加任务schedule()。
实现步骤:
1、描述任务:
具体的实现逻辑Runnable表示,执行的时间可以用一个long类型的time去记录
//描述定时器的执行任务 class MyTask implements Comparable<MyTask>{ private Runnable runnable; //定义执行器的执行时间 private long time; public MyTask(Runnable runnable,Long time){ if(time<0){ throw new RuntimeException("延迟时间不能小于0"); } this.runnable=runnable; //time记录当前任务的具体执行时间 this.time=time+System.currentTimeMillis(); }
2、组织任务:
用一个阻塞队列去组织任务
//使用优先级队列保存任务 private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
3、提供一个提交任务的方法
//定义一个添加任务的方法 public void schedule(Runnable runnable,Long time) throws InterruptedException { //封装成任务对象 MyTask myTask=new MyTask(runnable,time); //加入队列中 queue.put(myTask); //唤醒线程 synchronized(locker){ locker.notifyAll(); } }
4、创建一个扫描线程不停的去扫描队列是否有执行的任务
//创建扫描线程 Thread thread=new Thread(() ->{ while(true){ try { //1、取出任务 MyTask task = queue.take(); //2、判断任务时间是不是已经到了 Long currentTime=System.currentTimeMillis(); if(currentTime>=task.getTime()){ //如果时间到了就执行任务 task.getRunnable().run(); }else{ //如果时间没有到,就再写入队列 queue.put(task); //计算一下执行任务时间与当前时间的差值 long waitTime=task.getTime()-currentTime; synchronized(locker){ //继续等待waitTime时间 locker.wait(waitTime); } } } catch (InterruptedException e) { e.printStackTrace(); } } },"taskScanner"); //启动线程 thread.start();
5、定时器的延迟时间不能为负数
public MyTask(Runnable runnable,Long time){
if(time<0){
throw new RuntimeException("延迟时间不能小于0");
}
this.runnable=runnable;
//time记录当前任务的具体执行时间
this.time=time+System.currentTimeMillis();
}
在任务的构造方法中加入时间不能为负的判断
6、Long类型向int类型转换的过程中可能会存在溢出问题
@Override public int compareTo(MyTask task) { //防止long转int的溢出问题 if(this.time==task.getTime()){ return 0; } if(this.time>task.getTime()){ return 1; }else{ return -1; } }
6、解决忙等问题
如果任务1执行完成之后,那么这个线程就会一直循环查看这个阻塞队列,不带停的。等到任务2的时间到了才判断出执行任务时间到了,那么中间这一段时间是没有必要去等的,而且非常的浪费CPU资源,这个现象就叫”忙等“。
如何解决:
1、取出任务判断一下如果没有到执行时间,计算一下,任务的执行时间与当前时间的差值。
2、把任务添加到队列中。
3、wait(等待差值时间),等待时间一到,刚好时执行任务的时间。
通过设置wait解决了忙等问题。
7、解决无法唤醒问题
在每次添加完任务之后加一个notify,来唤醒线程
8、CPU调度过程中可能会产生执行顺序问题
造成上述现象是因为没有保证原子性
如何保证原子性?
完整代码:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 自定义实现定时器
*/
public class MyTimer {
//使用优先级队列保存任务
private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//定义一个锁对象
Object locker=new Object();
//定义一个添加任务的方法
public void schedule(Runnable runnable,long time) throws InterruptedException {
//封装成任务对象
MyTask myTask=new MyTask(runnable,time);
//加入队列中
queue.put(myTask);
//唤醒线程
synchronized(locker){
locker.notifyAll();
}
}
public MyTimer(){
//创建扫描线程
Thread thread=new Thread(() ->{
while(true){
try {
synchronized (locker) {
//1、取出任务
MyTask task = queue.take();
//2、判断任务时间是不是已经到了
Long currentTime = System.currentTimeMillis();
if (currentTime >= task.getTime()) {
//如果时间到了就执行任务
task.getRunnable().run();
} else {
//如果时间没有到,就再写入队列
queue.put(task);
//计算一下执行任务时间与当前时间的差值
long waitTime = task.getTime() - currentTime;
synchronized (locker) {
//继续等待waitTime时间
locker.wait(waitTime);
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"taskScanner");
//启动线程
thread.start();
}
}
//描述定时器的执行任务
class MyTask implements Comparable<MyTask>{
private Runnable runnable;
//定义执行器的执行时间
private long time;
public MyTask(Runnable runnable,Long time){
if(time<0){
throw new RuntimeException("延迟时间不能小于0");
}
this.runnable=runnable;
//time记录当前任务的具体执行时间
this.time=time+System.currentTimeMillis();
}
@Override
public int compareTo(MyTask task) {
//防止long转int的溢出问题
if(this.time==task.getTime()){
return 0;
}
if(this.time>task.getTime()){
return 1;
}else{
return -1;
}
}
public Runnable getRunnable(){
return runnable;
}
public Long getTime(){
return time;
}
}
/**
* 测试定时器
*/
public class Exe_02 {
public static void main(String[] args) throws InterruptedException {
//创建一个定时器对象
MyTimer timer=new MyTimer();
//添加定时任务
timer.schedule(()->{
System.out.println("任务1启动,三万次出枪");
},1000);
timer.schedule(()->{
System.out.println("任务2启动,鸡与清风");
},3000);
timer.schedule(()->{
System.out.println("任务3启动,三万出枪");
},5000);
timer.schedule(()->{
System.out.println("任务4启动,三万出枪中");
},8000);
timer.schedule(()->{
System.out.println("三万累了休息中");
},6000);
timer.schedule(()->{
System.out.println("结束任务,三万收枪,执行时间3秒");
},9000);
}
}
运行结果:
解决死锁问题 :
代码示例:
public class Exe_03 {
public static void main(String[] args) throws InterruptedException {
MyTimer myTimer=new MyTimer();
myTimer.schedule(() -> {
System.out.println("任务 1");
}, 0);
// 2 秒
myTimer.schedule(() -> {
System.out.println("任务 2");
}, 0);
myTimer.schedule(() -> {
System.out.println("任务 3");
}, 0);
}
}
运行结果:
导致死锁的原因:
1、在扫描线程中先拿到synchronized,获取阻塞队列的任务。
2、当阻塞队列为空的时间,阻塞队列进入wait状态,等待队列不为空时才能被唤醒。
3、于是代码就阻塞在了。
4、阻塞在这里,那么扫描线程的synchronized就无法退出,也就无法释放锁。
5、导致了schedule方法阻塞在synchronized获取锁资源里,无法向下执行代码。
6、两个线程都在等待,造成死锁现象。
这时,可以单独创建一个唤醒线程,让任务2和任务3不再出现死锁的现象,并且缩小执行线程任务synchronized的范围
代码优化:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 自定义实现定时器
*/
public class MyTimer {
//使用优先级队列保存任务
private BlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//定义一个锁对象
Object locker=new Object();
//定义一个添加任务的方法
public void schedule(Runnable runnable,long time) throws InterruptedException {
//封装成任务对象
MyTask myTask=new MyTask(runnable,time);
//加入队列中
queue.put(myTask);
//唤醒线程
synchronized(locker){
locker.notifyAll();
}
}
public MyTimer(){
//创建扫描线程
Thread thread=new Thread(() ->{
while(true){
try {
//1、取出任务
MyTask task = queue.take();
//2、判断任务时间是不是已经到了
Long currentTime = System.currentTimeMillis();
if (currentTime >= task.getTime()) {
//如果时间到了就执行任务
task.getRunnable().run();
} else {
//如果时间没有到,就再写入队列
queue.put(task);
//计算一下执行任务时间与当前时间的差值
long waitTime = task.getTime() - currentTime;
synchronized (locker) {
//继续等待waitTime时间
locker.wait(waitTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"taskScanner");
//启动线程
thread.start();
//创建一个后台线程用来唤醒线程
Thread daemonThread=new Thread(() ->{
while(true){
synchronized(locker){
//唤醒所有等待线程
locker.notifyAll();
}
try {
//睡眠一秒钟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//设置为后台线程
daemonThread.setDaemon(true);
daemonThread.start();
}
}
//描述定时器的执行任务
class MyTask implements Comparable<MyTask>{
private Runnable runnable;
//定义执行器的执行时间
private long time;
public MyTask(Runnable runnable,Long time){
if(time<0){
throw new RuntimeException("延迟时间不能小于0");
}
this.runnable=runnable;
//time记录当前任务的具体执行时间
this.time=time+System.currentTimeMillis();
}
@Override
public int compareTo(MyTask task) {
//防止long转int的溢出问题
if(this.time==task.getTime()){
return 0;
}
if(this.time>task.getTime()){
return 1;
}else{
return -1;
}
}
public Runnable getRunnable(){
return runnable;
}
public Long getTime(){
return time;
}
}
将代码重新运行就会看到死锁现象已经被解决了。