Timer中最常用的方法几乎就是schedule方法,需要传递两个参数,一个是TimerTask的实例对象,另一个是多久后运行,其中TimerTask是一个接口,其实现了Runnable接口,所以通过TimerTask创建的实现类实例对象中也是有run方法的。
如果任务特别多,创建多个线程造成的开销是很大的,所以只需要只有一个/一组工作线程,每次都找到这些任务中最先到达时间的任务这就涉及到先后的问题,排序比较低效,而且插入任务的时候也是个麻烦,最好的是使用小根堆,即优先级队列,考虑到线程安全,要使用优先级阻塞队列即PriorityBlockingQueue
创建自定义类记录执行任务与执行时间
在使用优先级阻塞队列时,要创建一个新的自定义类,用于记录要执行的任务,以及执行的时间,将该类的每个实例为单位put进优先级阻塞队列中:
class MyTask implements Comparable<MyTask>{
public Runnable runnable;
//为方便使用,使用绝对时间戳
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
//取当前时刻时间戳+delay,作为该任务实际开始执行的时间戳
this.time = delay + System.currentTimeMillis();
}
@Override
public int compareTo(MyTask o) {
return (int)(time - o.time);
}
}
这里实现了Comparable接口,是为了在优先级阻塞队列中进行正确的比较,当然也可以在创建优先级阻塞队列的时候传递比较器,比如使用lambda或者内部类的方法
简单模拟Timer
成员变量
locker用于后续上锁与释放锁,queue是一个优先级阻塞队列,默认是小根堆,因为MyTask实现了Comparable接口,所以这里不传递比较器
Object locker = new Object();
//优先级阻塞队列中存储MyTask方便查看任务与任务开始的时间
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(
/*11,((o1, o2) -> (int)(o1.time - o2.time))*/);
构造方法与schedule方法
public void schedule(Runnable runnable, long delay){
MyTask task = new MyTask(runnable,delay);
queue.put(task);
synchronized (locker){
//如果MyTimer的前台线程在WAITING状态就将其唤醒,将堆顶的任务时间与新插入的任务的任务时间进行比较
locker.notify();
}
}
public MyTimer() {
Thread thread = new Thread(() -> {
while (true){
try {
synchronized (locker) {
MyTask task = queue.take();
//Thread demo = new Thread(task.runnable);
/*这里如果采用创建线程的方法,就不会因为某个任务执行时间过长,使得后续任务延期*/
/*但是创建多个线程开销过大,所以采用调用run的方法,并没用真正创建线程*/
long curTime = System.currentTimeMillis();
if (curTime >= task.time){
//如果当前时间戳>=任务设定时的时间戳 + delay
task.runnable.run();
//demo.start();
}else {
//还没有到时间,就把拿出来的任务放回去,并且等待时长为时间差的一段时间
queue.put(task);
locker.wait(task.time - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
上述代码中获取的时间戳是当前时刻与基准时刻的毫秒差,所以时间越往后,时间戳越大。基准时刻是1970年1月1日00:00:00
上锁位置问题
如果单独为了wait进行局部上锁,有可能会导致notify失效,从而导致任务执行时间安排的错乱
总体效果:
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{
public Runnable runnable;
//为方便使用,使用绝对时间戳
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
//取当前时刻时间戳+delay,作为该任务实际开始执行的时间戳
this.time = delay + System.currentTimeMillis();
}
@Override
public int compareTo(MyTask o) {
return (int)(time - o.time);
}
}
class MyTimer{
Object locker = new Object();
//优先级阻塞队列中存储MyTask方便查看任务与任务开始的时间
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>(
/*11,((o1, o2) -> (int)(o1.time - o2.time))*/);
public void schedule(Runnable runnable, long delay){
MyTask task = new MyTask(runnable,delay);
queue.put(task);
synchronized (locker){
//如果MyTimer的前台线程在WAITING状态就将其唤醒,将堆顶的任务时间与新插入的任务的任务时间进行比较
locker.notify();
}
}
public MyTimer() {
Thread thread = new Thread(() -> {
while (true){
try {
synchronized (locker) {
MyTask task = queue.take();
//Thread demo = new Thread(task.runnable);
/*这里如果采用创建线程的方法,就不会因为某个任务执行时间过长,使得后续任务延期*/
/*但是创建多个线程开销过大,所以采用调用run的方法,并没用真正创建线程*/
long curTime = System.currentTimeMillis();
if (curTime >= task.time){
//如果当前时间戳>=任务设定时的时间戳 + delay
task.runnable.run();
//demo.start();
}else {
//还没有到时间,就把拿出来的任务放回去,并且等待时长为时间差的一段时间
queue.put(task);
locker.wait(task.time - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
运行效果
由于第一个安排的任务是一个死循环,所以后续两任务都是执行不到的,也就是说,并不是安排多久执行就多久执行,而是根据上传的时间参数和上一个任务结束的具体时间安排的,真正的Timer也是这样的
class ThreadDemo11 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
while (true){}
}
},1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello2");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello3");
}
},3000);
}
}
如果把第一个安排的任务死循环去除,那么后续的任务是可以及时执行的
class ThreadDemo11 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello1");
}
},1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello2");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello3");
}
},3000);
}
}
这里执行完3个任务,程序并没有真正停止,因为根据构造方法创造的thread因为在优先级阻塞队列内没有元素的情况下尝试take进入阻塞态,该线程并没有结束 ,而真正的Timer也是在执行完所有任务后,内部的线程不会停止,需要其他途径关闭
目录