1. 前言
在Java中,定时器
Timer
类是用于执行定时任务的工具类。它允许你安排一个任务在未来的某个时间点执行,或者以固定的时间间隔重复执行。
在服务器开发中,客户端向服务器发送请求,然后等待服务器响应. 但服务器什么时候返回响应,并不确定. 但也不能让客户端一直等下去, 如果一直死等,就没有意义了. 因此通常客户端会通过定时器
设置一个"等待的最长时间".
2. 定时器的基本使用
Java的标准库库中就给我们提供了一个定时器Timer类
可以看到Timer
这个类在很多包里面都有,注意要选择java.util
里的
其中在Timer类中有一个十分重要的方法- schedule()
方法
形参:
-
task
:要执行的任务,必须是TimerTask
的子类,可以通过继承TimerTask
类并重写run()
方法来定义具体的任务逻辑。 -
time
:指定任务执行的时间,类型为java.util.Date
。
当然一个Timer类中也可以执行设置多个任务.
示例:
public class Demo17 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("1s!");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("2s!");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3s!");
}
},3000);
}
}
运行结果:
仔细观察运行结果,会发现这个程序有些问题,为什么程序执行完了,进程没有退出呢?
是因为Timer内部需要一组线程来执行注册任务,这里的线程是前台线程,会影响进程的退出
3. 实现定时器
实现定时器,最主要的就是实现里面的schedule
方法
class MyTask{
// 要执行的任务
private Runnable runnable;
// 时间
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + time;
}
}
public class MyTimer {
public void schedule(Runnable runnable, long time){
MyTask myTask = new MyTask(runnable,time);
}
}
System.currentTimeMillis()
是Java中的一个静态方法,用于获取当前时间的毫秒数。
描述一个任务,以及多久后执行定时器的第一步完成了
接下来就是如何让这个定时器能够管理多个任务,例如上述示例中输出1s,2s,3s的那个示例一样
关于如何管理这些任务,我们肯定是想让设置时间短的任务先执行,但是在设置任务时,不一定会按照时间从小到大的顺序去进行放入. 这时候就要使用到 优先级队列(PriorityQueue)
但是优先级队列并不是线程安全的, 在多线程环境下使用优先级队列可能会出现问题,我们可以使用阻塞队列
不要忘了,我们可以创建一个带有优先级的阻塞队列
将任务添加到阻塞队列中即可.
但是优先级队列的对象的类型必须是可比较的. 我们可以让Mytask
实现Comparable
接口,实现里面的compareTo
方法. 比较的规则就是时间,时间小的优先级高.
接下来就要检查队首任务的时间是否到了,时间到了就要执行任务. 可以单独创建一个扫描线程来进行检查.
完整代码:
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 time) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + time;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time);
}
}
public class MyTimer {
private BlockingQueue<MyTask> blockingQueue = new PriorityBlockingQueue<>();
public MyTimer() {
Thread t = new Thread(()->{
while(true){
try {
MyTask myTask = blockingQueue.take();
// 当前时间是否大于等于要执行任务的时间
if (System.currentTimeMillis() >= myTask.getTime()){
// 时间到了 执行任务
myTask.getRunnable().run();
}else {
// 时间没到,再把任务放回阻塞队列中
blockingQueue.put(myTask);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
public void schedule(Runnable runnable, long time) throws InterruptedException {
MyTask myTask = new MyTask(runnable,time);
blockingQueue.put(myTask);
}
}
测试代码:
public class Demo18 {
public static void main(String[] args) throws InterruptedException {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2s");
}
},2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1s");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3s");
}
},3000);
}
}
运行结果:
结果没有问题.
4. 优化上述的定时器代码
但仔细思考上述代码中还存在一个问题:
这里的条件是while(true),说明程序会一直进行这里的循环, 这也是"忙等"
"忙等"是指一个线程在等待某个条件满足时,不断地进行无效的循环检查,而不释放CPU资源给其他线程执行。这种方式会浪费CPU资源,并且可能导致性能下降。
针对这个问题,我们可以使用 wait和notify
来解决这个问题
通过使用wait
和notify
,对MyTimer
这个类进行优化:
public class MyTimer {
private BlockingQueue<MyTask> blockingQueue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public MyTimer() {
Thread t = new Thread(()->{
while(true){
try {
MyTask myTask = blockingQueue.take();
// 当前时间是否大于等于要执行任务的时间
if (System.currentTimeMillis() >= myTask.getTime()){
// 时间到了 执行任务
myTask.getRunnable().run();
}else {
// 时间没到,再把任务放回阻塞队列中
blockingQueue.put(myTask);
// 进行等待
synchronized (locker) {
locker.wait(myTask.getTime()-System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
public void schedule(Runnable runnable, long time) throws InterruptedException {
MyTask myTask = new MyTask(runnable,time);
blockingQueue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
}
虽然解决了"忙等"问题,但是又带来了新的问题.
如果扫描线程再取出队首任务(10分钟后要执行)时,线程切换,执行
schedule
方法,新增任务(5分钟后执行)然后执行notify
,但此时并没有通知线程并没有意义,因为扫描线程刚执行完take,并没有执行到wait
,然后扫描线程继续执行,进行wait
,等待10分钟. 这样就会把刚才新增的5分钟后执行的任务给错过了.
对于上述问题 产生的原因还是因为"锁"的粒度不够大, 这些操作不是原子的,只需放大锁的粒度即可
public class MyTimer {
private BlockingQueue<MyTask> blockingQueue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public MyTimer() {
Thread t = new Thread(()->{
while(true){
try {
synchronized (locker) {
MyTask myTask = blockingQueue.take();
// 当前时间是否大于等于要执行任务的时间
if (System.currentTimeMillis() >= myTask.getTime()){
// 时间到了 执行任务
myTask.getRunnable().run();
}else {
// 时间没到,再把任务放回阻塞队列中
blockingQueue.put(myTask);
// 进行等待
locker.wait(myTask.getTime()-System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
public void schedule(Runnable runnable, long time) throws InterruptedException {
MyTask myTask = new MyTask(runnable,time);
blockingQueue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
}
上述为了解决"忙等"问题,使用wait和notify进行优化,而在优化过程因为synchronized
加锁的范围不一样,又带来了新的问题. 因此多线程问题很复杂,加锁的范围,线程的切换都会影响程序的执行效果.
5. 总结
文章主要介绍了定时器的基本使用,以及自定义实现定时器,实现一个定时器并不难.但如果要想将定时器实现的更好,也不是一件容易的事. 毕竟多线程环境中,很容易出现各种意想不到的问题.
文章来源:https://www.toymoban.com/news/detail-731191.html
感谢你的观看!希望这篇文章能帮到你!
专栏: 《从零开始的Java学习之旅》在不断更新中,欢迎订阅!
“愿与君共勉,携手共进!”
文章来源地址https://www.toymoban.com/news/detail-731191.html
到了这里,关于【Java|多线程与高并发】定时器(Timer)详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!