1. 标准库中的定时器
标准库中提供了⼀个 Timer 类,Timer 类的核心方法为 schedule;
schedule 包含两个参数,第⼀个参数指定即将要执⾏的任务代码,第二个参数指定多长时间之后执行 (单位为毫秒);
public class Demo_801 {
public static void main(String[] args) {
// JDK中的类,创建一个定时器
Timer timer = new Timer();
// 向定时器中添加任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务1");
}
}, 1000); // 1秒后执行任务1
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务2");
}
}, 2000); // 2秒后执行任务2
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务3");
}
}, 3000); // 3秒后执行任务3
}
}
执行结果:

执行完已有任务之后,就阻塞等待新的任务。
2. 自定义实现定时器
步骤:
- 用一个类来描述任务和执行任务的时间
具体任务的逻辑用Runable表示,执行时间的可以用一个long型delay去表示

- 组织任务和时间对应的对象
使用阻塞队列

因此使用优先级阻塞队列

- 提供一个方法提交任务

- 定义一个线程执行任务
将线程定义在构造方法中

当前代码:
/**
* 自定义定时器
*/
public class MyTimer {
// 用一个阻塞队列来组织任务
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public MyTimer() {
// 创建扫描线程
Thread thread = new Thread(() -> {
while (true) {
try {
// 1. 从队列中取出任务
MyTask take = queue.take();
// 2. 判断任务是否到达执行时间
long currentTime = System.currentTimeMillis();
if (currentTime >= take.getTime()) {
// 如果时间到了则执行任务
take.getRunnable().run();
} else {
// 如果时间没到则把任务再放回到队列中
queue.put(take);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
thread.start();
}
/**
* 添加任务的方法
*/
public void schedule (Runnable runnable, long delay) throws InterruptedException {
// 构造MyTask
MyTask myTask = new MyTask(runnable, delay);
// 把任务放进阻塞队列中
queue.put(myTask);
}
}
// 1. 用一个类来描述任务和执行任务的时间
class MyTask implements Comparable<MyTask> {
// 任务
private Runnable runnable;
// 任务执行的时间
private long time;
public MyTask(Runnable runnable, long delay) {
// 校验任务不能为空
if (runnable == null) {
throw new IllegalArgumentException("任务不能为空.");
}
// 时间不能为负数
if (delay < 0) {
throw new IllegalArgumentException("执行时间不能小于0.");
}
this.runnable = runnable;
// 计算出任务执行的具体时间
this.time = delay + System.currentTimeMillis();
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
if (this.getTime() > o.getTime()) {
return 1;
} else if (this.getTime() < o.getTime()) {
return -1;
} else {
return 0;
}
}
}
调用代码:
public class Demo_802 {
public static void main(String[] args) throws InterruptedException {
// 创建定时器对象
MyTimer timer = new MyTimer();
// 向定时器中添加任务
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
}, 1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务2");
}
}, 2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务3");
}
}, 3000);
}
}
执行结果:

目前看来代码执行结果是正确的,但是代码中依然存在一些比较严重的问题.
3. 代码中存在的问题
3.1 忙等

如图所示,如果当前时间是6点,执行时间最早的任务为7点,若按照当前代码的逻辑,线程会不停的从队列中取出任务,检查执行时间,然后放回阻塞队列,但是这都是无用功,而且while(true)会一直消耗系统资源,看起来很忙但都是无意义的,这就是 “忙等”。

解决办法:通过 wait(time) 的方式让程序等待一段时间,等待时间就是当前时间与下一个执行任务的时间的差。

在添加任务时唤醒一下线程,重新计算等待时间

也就解决了忙等问题,但是还有其他问题。
3.2 线程调度问题
当线程执行到一半被CPU调度走:


由于线程调度的问题,t2先入队了新任务,执行时间在t1读取的任务执行时间之前,t1读的任务发现时间没有到放回队列的时候,设置的等待时间超过了新任务的执行时间,导致t2放入队列的新任务不能及时的执行。
造成这个现象的原因是没有保证原子性!
解决办法:加大锁的粒度

此时保证了原子性,不会出现中途被CPU调度走的问题。
但此时又引出了新的问题。
3.3 同时添加时间为0的任务(难点)

当同时添加0秒后执行的任务,打印结果为:

可见,定时器只执行了一个任务之后就阻塞等待了。
原因:
-
首先创建了一个定时器对象;
-
向定时器中添加了第一个任务,因为new对象是在JVM层面的,当启动线程时,阻塞队列中已经添加了一个任务;
-
扫描线程启动,处理第一个任务,打印出“马上执行任务1”;
-
扫描线程立即循环,获取第二个任务时,发现阻塞队列此时是空的(由于线程调度的不确定性,主线程尚未提交后续任务,扫描线程就已进入下一轮等待),开始阻塞等待,同时扫描线程获取到了锁对象;

-
主线程向队列中添加任务的时候,等待扫描线程的锁对象,由于扫描线程无法释放锁对象,主线程也就获取不到锁对象,形成锁被长时间持有导致的阻塞;

不是因为时间设置为0才会阻塞,是因为添加任务的执行时间间隔过短!
流程图:

解决办法:
在处理任务无法及时执行的问题时,扩大了加锁的范围,却又引入了更大的问题一般我们两害相全取其轻;
因此还是将锁的粒度缩小,因为造成长时间死锁问题比无法及时执行任务严重的多;
但为了解决无法及时执行任务的问题,可以创建一个后台的扫描线程,只做定时唤醒操作定时1秒或10ms,唤醒一次;
后台线程不会影响前台线程,不随着主线程的退出而退出。
public MyTimer() {
// 创建扫描线程
Thread thread = new Thread(() -> {
while (true) {
try {
// 1. 从队列中取出任务
MyTask take = queue.take();
// 2. 判断任务是否到达执行时间
long currentTime = System.currentTimeMillis();
if (currentTime >= take.getTime()) {
// 如果时间到了则执行任务
take.getRunnable().run();
} else {
// 当前时间与任务执行时间的差
long waitTime = take.getTime() - currentTime;
// 如果时间没到则把任务再放回到队列中
queue.put(take);
synchronized (locker) { // 减小锁粒度!!!!!!!!!!!
// 等待时间
locker.wait(waitTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
thread.start();
// 创建一个后台线程
Thread deamonThread = new Thread(() -> {
while (true) {
synchronized (locker) {
locker.notifyAll();
}
// 休眠一会儿
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 设置为后台线程
deamonThread.setDaemon(true);
// 启动线程
deamonThread.start();
}
执行结果:

这种处理方式,首先可以保证正常业务的运行,又兼顾了小概率的事件
完整代码
MyTimer:
/**
* 自定义定时器
*/
public class MyTimer {
// 用一个阻塞队列来组织任务
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
// 锁对象
private Object locker = new Object();
public MyTimer() {
// 创建扫描线程
Thread thread = new Thread(() -> {
while (true) {
try {
// 1. 从队列中取出任务
MyTask take = queue.take();
// 2. 判断任务是否到达执行时间
long currentTime = System.currentTimeMillis();
if (currentTime >= take.getTime()) {
// 如果时间到了则执行任务
take.getRunnable().run();
} else {
// 当前时间与任务执行时间的差
long waitTime = take.getTime() - currentTime;
// 如果时间没到则把任务再放回到队列中
queue.put(take);
synchronized (locker) {
// 等待时间
locker.wait(waitTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
thread.start();
// 创建一个后台线程
Thread deamonThread = new Thread(() -> {
while (true) {
synchronized (locker) {
locker.notifyAll();
}
// 休眠一会儿
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 设置为后台线程
deamonThread.setDaemon(true);
// 启动线程
deamonThread.start();
}
/**
* 添加任务的方法
*/
public void schedule(Runnable runnable, long delay) throws InterruptedException {
// 构造MyTask
MyTask myTask = new MyTask(runnable, delay);
// 把任务放进阻塞队列中
queue.put(myTask);
synchronized (locker) {
// 唤醒等待的线程
locker.notifyAll();
}
}
}
// 1. 用一个类来描述任务和执行任务的时间
class MyTask implements Comparable<MyTask> {
// 任务
private Runnable runnable;
// 任务执行的时间
private long time;
public MyTask(Runnable runnable, long delay) {
// 校验任务不能为空
if (runnable == null) {
throw new IllegalArgumentException("任务不能为空.");
}
// 时间不能为负数
if (delay < 0) {
throw new IllegalArgumentException("执行时间不能小于0.");
}
this.runnable = runnable;
// 计算出任务执行的具体时间
this.time = delay + System.currentTimeMillis();
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
if (this.getTime() > o.getTime()) {
return 1;
} else if (this.getTime() < o.getTime()) {
return -1;
} else {
return 0;
}
}
}
Main方法:
public class Demo_802 {
public static void main(String[] args) throws InterruptedException {
// 创建定时器对象
MyTimer timer = new MyTimer();
// 向定时器中添加任务
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("马上执行任务1");
}
}, 0);
// 向定时器中添加任务
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("马上执行任务2");
}
}, 0);
// 向定时器中添加任务
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("马上执行任务3");
}
}, 0);
// 向定时器中添加任务
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
}, 1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务2");
}
}, 2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务3");
}
}, 3000);
}
}

7万+

被折叠的 条评论
为什么被折叠?



