目录
▮定时器实例
定时器是一个非常实用的一个工具,你能给定它一个任务和一个倒计时,等倒计时一到,它就会自动去执行这个任务。
首先,定时器是java.util包中一个的一个类:Timer,要先有一个对象才能使用。
public class demo01 {
public static void main(String[] args) {
Timer timer = new Timer();
}
}
然后,使用方法:schedual()来添加任务和倒计时。这个方法有两个参数(任务,倒计时)。任务的类型是TimerTask抽象类,而倒计时是一个long。 TimerTask和Runnable接口很像,可以重写run()来写入任务代码;倒计时是以毫秒为单位,1000就等于1s。
public class demo01 {
public static void main(String[] args) {
Timer timer = new Timer();
//tiem.schedule(任务,倒计时);
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("一秒后打印:任务1");
}
},1000);
}
}
这样一来,一个简单的定时器实例就完成了。程序执行1s后,会自动打印一个“一秒后打印:任务1”。
•TimerTask
public abstract class TimerTask implements Runnable { //其它成员 }
TimerTask是一个实现了Runnable接口的抽象类,所以可以把它当作Runnabl接口,通过重写里面的run()方法来实现添加任务
还有一点需要强调,定时器执行完任务后并不会结束程序,而是在那等待。因为一个定时器可能不止安排一个任务,所以一个定时器执行玩所有任务后会进入待机状态。
public class demo01 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("一秒后打印:任务1");
}
},1000);
//同一个定时器添加了第二个任务
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("二秒后打印:任务2");
}
},2000);
}
}
任务结束后,程序并没有出现下方结束的标志。所以说定时器结束任务后进程不会结束
![]()
▮定时器的内部实现原理
class Timer{
//1.一个阻塞优先级队列
//2.一个静态内部类Task
//3.方法schedule(任务,时间)
//4.一个进程,执行定时器
//5.一个锁
//6.Timer的构造方法。
}
上面就是一个定时器的6个核心部位,接下来我们来一一讲解。
▪阻塞优先级队列
因为一个定时器里面可以安排多个任务,所以使用“队列”数据结构来装入任务。又因为要按照倒计时的大小对任务进行排序,所以使用优先级队列也就是堆结构来排序。最后,使用阻塞队列来让定时器能进入待机状态,不会因为任务结束而停止定时器。
Java中给我们提供了阻塞优先级队列,所以我们可以直接使用,至于它内部的实现原理,我们暂时不谈,知道阻塞队列是什么,就差不多知道这是个什么了。
class MyTimer{
//PriortyBlockingQueue<T>:阻塞优先级队列
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
}
▪静态内部类Task
上面我们看到,阻塞优先级队列的泛型是Task。所以说,这个内部类就是要往队列里添加的元素。
一个任务有两个成员,一个是他的行为,一个是他的倒计时。行为用Thread来接收,倒计时用long
private static class Task {
Thread task;//任务
long time;//倒计时
}
再给个构造方法,时间我们取现在的时间+给出的倒计时。构造方法在定时器被安排任务的那一刻启动,这样就求得了任务执行的具体时间,而不再是一个倒计时。
private static class Task{
Thread task;
long time;
//构造方法
public Task(Runnable runnable,long time){
task = new Thread(runnable);
this.time = System.currentTimeMillis() + time;
}
}
}
最后,不要忘记了我们的Task是要添加进优先级队列的,我们引入比较接口来为task提供比较的方法,让队列能把时间短的任务给排到队头。
private static class Task implements Comparable<Task>{//泛型Task,因为要跟其它Task比较
Thread task;//任务
long time;//具体执行时间,不再是倒计时
//构造方法
public Task(Runnable runnable,long time){
task = new Thread(runnable);
this.time = System.currentTimeMillis() + time;
}
//比较方法
@Override
public int compareTo(Task o) {
return (int)(time - o.time);//记得转成int类型
}
//提供一个run()方法来执行里面的任务
public void run(){
task.run();
}
}
}
▪方法 schedule(任务,倒计时)
这个方法是用于定时器安排任务的,所以要创建一个Task对象来接收这个任务,然后将任务添加队列。
public void schedule(Runnable command,long after){
//创建Task对象
Task task = new Task(command,after);
//将对象加入阻塞优先级队列
queue.offer(task);
/* 这段下文会解释,目前不管
synchronized (locking){
locking.notify();
}
*/
}
▪一个进程,执行定时器
新建一个线程来执行定时器。定时器执行的逻辑是:取得目前时间和列头任务;如果当前时间大于等于任务时间,则执行此任务;反之则将此任务再次添入队列。最后,while重复执行上述逻辑。
private Thread t = new Thread(() -> {
while (true) {
//当前时间
long curTime = System.currentTimeMillis();
//队头任务
Task task = queue.take();
if(curTime >= task.time){
task.run();
}else{
queue.offer(task);
}
}
});
这里有个盲等的问题:如果,当前任务时间未到;那么,这个任务会被丢到队列;接着,优先级队列又把它给排到队头。一直这样重复到任务时间到。这种盲等的问题是毫无意义的,还会占用CPU资源。所以我们创建一个锁对象,然后使用wait()和notify()来解决这个问题。给wait传入(任务时间-现在时间),等待任务执行前的空白时间。
//创建了一个锁对象
Object locking = new Object();
private Thread t = new Thread(() -> {
while (true) {
synchronized (locking) {//上锁
try {//wait()方法所需的返回异常
long curTime = System.currentTimeMillis();
Task task = queue.take();
if(task.time <= curTime){
task.run();
}else{
queue.offer(task);
//等待任务前的空白时间
locking.wait(task.time - curTime);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
因为wait有限定时间,所以可以不用notify()唤醒的。但是呢有一种情况,就是在等待的过程中,定时器又被重新安排一个任务,这个任务是一个更早执行的任务。如果此时定时器还在等待上一个任务,那就会错过这个新任务的执行时间。所以我们在schedual里添加一个notify(),在每次添加新任务后,都重新对队头任务进行判断。
public void schedule(Runnable command,long after){
Task task = new Task(command,after);
queue.offer(task);
//唤醒wait
synchronized (locking){
locking.notify();
}
}
▪构造方法
定时器的构造方法很简单,就是在定时器创建时,启动定时器线程。
public MyTimer(){
t.start();
}
▪完整代码
import java.util.concurrent.PriorityBlockingQueue;
class MyTimer{
//构造方法
public MyTimer(){
t.start();
}
//PriortyBlockingQueue<T>:阻塞优先级队列
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
//方法:安排任务
public void schedule(Runnable command,long after){
//创建Task对象
Task task = new Task(command,after);
//将对象加入阻塞优先级队列
queue.offer(task);
// 这段下文会解释,目前不管
synchronized (locking){
locking.notify();
}
}
//创建了一个锁对象
Object locking = new Object();
//定时器进程
private Thread t = new Thread(() -> {
while (true) {
synchronized (locking) {//上锁
try {//wait()方法所需的返回异常
long curTime = System.currentTimeMillis();
Task task = queue.take();
if(task.time <= curTime){
task.run();
}else{
queue.offer(task);
//等待任务前的空白时间
locking.wait(task.time - curTime);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
//静态内部类 Task
private static class Task implements Comparable<Task>{//泛型Task,因为要跟其它Task比较
Thread task;//任务
long time;//具体执行时间,不再是倒计时
//构造方法
public Task(Runnable runnable,long time){
task = new Thread(runnable);
this.time = System.currentTimeMillis() + time;
}
//比较方法
@Override
public int compareTo(Task o) {
return (int)(time - o.time);//记得转成int类型
}
//提供一个run()方法来执行里面的任务
public void run(){
task.run();
}
}
}
▮定时器的一些细节
▪任务的传递
首先,在给定时器添加任务的时候,我们使用了TimerTask这个类来定义任务。因为TImerTask实现了Runnable接口,所以我们本质上还是在使用Runnable;然后,在schedule()中,我们用了一个Runnable的形参来接收任务对象;最后,我们把这个TimerTask对象赋给了一个类型Thread的引用。
一个TimerTask对象,重写了Runnable接口的run(),最后传给了一个Thread的引用。
new TimerTask() {
@Override
public void run() {
System.out.println(1);
}
}
public void schedule(Runnable command,long after){
Task task = new Task(command,after);
}
public Task(Runnable command,long time){
command = new Thread(runnable);
this.time = System.currentTimeMillis() + time;
}
▪多线程理解
定时器里只有一个线程,定时器在这个线程里执行的是任务的run()方法,而不是给任务开创新的线程去执行。
schedule()方法是其它线程在调用,不在定时器线程中使用。任务的执行由定时器来负责,而任务的安排由使用定时器的线程来安排。
▪锁所起到的作用
这里面的锁起到了两个作用,一个是保护了代码的原子性,一个是wait()来等待。这里的锁并没有起到防止竞争的作用,因为根本就没有其它线程来跟定时器竞争任务。
wait()等待作用已经在上面讲过了,那我们就细谈一些忙等问题。下方代码中,锁的位置有所不同。案例1是我们正确的写法,而案例2没能保护代码原子性。为什么了?
假如定时器线程在取得当前时间curTime和任务task后,其它线程给定时器安排了一个时间比当前任务更早的任务。前面我们知道,schedaul()方法里有个notify(),这个notify()是用来唤醒当前任务的等待。但是吧,此时的定时器还没走到wait()这一步来,所以这个notify就唤醒了一个寂寞。可是,定时器中的wait()依旧执行。这样一来,我们新任务就要等待旧任务的空白时间。
这显然是存在问题的。问题就在于,定时器线程中,获得当前任务和时间代码和执行wait()代码不是原子的,所以我们使用了案例1这种写法。
/*
** 案例1
*/
private Thread t = new Thread(() -> {
while (true) {
synchronized (locking) {//上锁
try {
long curTime = System.currentTimeMillis();
Task task = queue.take();
if(task.time <= curTime){
task.run();
}else{
queue.offer(task);
locking.wait(task.time - curTime);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
/*
** 案例2
*/
private Thread t = new Thread(() -> {
while (true) {
long curTime = System.currentTimeMillis();
Task task = queue.take();
if(task.time <= curTime){
task.run();
}else{
queue.offer(task);
synchronized (locking){//上锁
try {
locking.wait(task.time - curTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});