java中的定时器

定时器的功能

在Java中,定时器用于在预定的时间执行任务,两种方式可以实现定时功能:Timer和TimerTask类,还有ScheduledExecutorService接口。

1.Timer和TimerTask

TimerTask是一个抽象类,需要创建它的子类并重写run()方法,这个方法可以在定时任务被执行时调用,Timer用于调度任务。

java 复制代码
import java.util.Timer;
import java.util.TimerTask;

public class MyTimerTask extends TimerTask {
    @Override
    public void run() {
        System.out.println("hello run");
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        MyTimerTask task = new MyTimerTask();
        System.out.println("hello main");
        timer.schedule(task, 1000, 2000);
    }
}

其中Java的Timer类中,schedule方法用于安排任务的执行,schdule(TimeTask task,long delay):其中task是要执行的任务,delay是任务首次执行的时间,单位是毫秒,且该任务只能被执行一次;schedule(TimeTask task,long delay,long period):period是任务连续执行之间的时间间隔,单位是毫秒,表示该任务首次执行是delay毫秒,之后每隔period毫秒重复执行。

当然,也可以使用匿名内部类的写法.TimerTask()创建了一个TimerTask的匿名内部类实例

java 复制代码
public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
        System.out.println("hello main");
    }

Timer和TimerTask遇到的问题

  1. 单线程执行:Timer使用单个后台程序来顺序地执行所有的定时任务,这意味着如果一个任务执行时间过长,那么它会延迟其他任务执行,如果一个任务抛出未捕获异常,那么整个Timer将会被终止,并且不会执行任何后续任务。

    java 复制代码
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class TimerExample {
    
    
        public static void main(String[] args) {
    
            Timer timer = new Timer();
    
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
            // 执行时间过长的任务
            TimerTask longRunningTask = new TimerTask() {
                @Override
                public void run() {
                    String formattedDateTime = getCurrentTime(formatter);
                    System.out.println("长任务开始: " + formattedDateTime);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    formattedDateTime = getCurrentTime(formatter);
                    System.out.println("长任务结束: " + formattedDateTime);
                }
            };
    
            // 正常的任务
            TimerTask normalTask = new TimerTask() {
                @Override
                public void run() {
                    String formattedDateTime = getCurrentTime(formatter);
                    System.out.println("正常任务: " + formattedDateTime);
                }
            };
    
            // 抛出异常的任务
            TimerTask exceptionThrowingTask = new TimerTask() {
                @Override
                public void run() {
                    String formattedDateTime = getCurrentTime(formatter);
                    System.out.println("抛异常任务开始时间 " + formattedDateTime);
                    throw new RuntimeException("这是一个未抛出的异常");
                }
            };
    
            // 安排任务
            timer.schedule(longRunningTask, 0);
            timer.schedule(normalTask, 2000);
            timer.schedule(exceptionThrowingTask, 4000);
    
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            timer.cancel();
        }
    
        private static String getCurrentTime(DateTimeFormatter formatter) {
            return LocalDateTime.now().format(formatter);
        }
    }

    长任务立即开始执行,并在5s后完成,正常任务本再2s后执行,但是由于长任务占据线程,被延迟到长任务完成后执行,抛异常任务本再4s执行,但是长任务占用线程,在长任务和正常任务完成后执行,没有执行抛出异常时,Timer会被终止后续任务不在执行

  2. 对系统时间敏感:系统时间被任务调整,那么会影响到Timer的调度逻辑,可能导致错过执行时间或者提前执行。

  3. 不适合长时间运行任务:如果任务执行时间超过了两次任务间隔的时间,那么下一层任务执行将会被推迟,直到当前任务完成,可能导致任务积压。

  4. 缺乏灵活性:Timer不支持复杂的调度需求。

  5. 资源管理:不取消不再需要的TimeTask或者关闭不再使用Timer,可能导致资源泄露

  6. 线程安全问题:Timer是线程安全的,但是TimerTask是非线程安全的。
    解释:TimerTask本身并不是非线程安全的,而是Timer的设计和使用方式导致潜在的线程安全问题,当涉及到共享资源或者状态时,可能存在线程安全问题。

2.ScheduledExecutorService接口

ScheduledExecutorService就是为了解决Timer&TimerTask的问题。它基于多线程机制,因此任务之间不会相互影响,内部使用了延时队列,基于等待/唤醒机制实现,因此CPU不会一直繁忙,同时多线程带来的CPU资源复用极大的提升性能

java 复制代码
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MyScheduledTask implements Runnable {
    @Override
    public void run() {
        System.out.println("hello run");
    }

    public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        MyScheduledTask task = new MyScheduledTask();

        scheduler.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);
        System.out.println("hello main");
    }
}

MyScheduledTask实现了Runnable接口,必须要重写Runnable的run()方法ScheduledExecutorService是ExecutorService的一个子接口,用于调度任务定时执行,当使用ScheduledExecutorService时,即使没有显式创建一个线程池 ,Java并发API内部也会管理一个线程池执行定时任务。(如上面的代码)

Executors.newScheduledThreadPool(int corePoolSize)创建一个具有指定核心线程数,还可以Executors.newSingleThreadScheduledExecutor()创建一个单线程ScheduledExecutorService,用于任务调度。

显式创建线程池是直接使用线程池的实现类(例如:ThreadPoolExecutor)来创建和管理线程池的过程,如下:

java 复制代码
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
       corePoolSize,
       maximumPoolSize,
       keepAliveTime,
       unit,
       workQueue
);

ScheduledExecutorService接口中的四个方法

scheduleAtFixedRate和scheduleWithFixedDelay的区别

scheduleAtFixedRate()以固定的(period)频率执行任务,如果任务执行时间超过了peirod,则下一个任务会在前一个任务完成后立即开始,不会等一个周期。如果短于period,则会在下一个period到达时执行任务。

scheduleWithFixedDelay在前一个任务完成后,等待一个固定的延迟时间,然后再执行下一个任务,因此执行任务时间和延时相加。

java 复制代码
public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(8);
        Runnable task = () -> {
            try {
                LocalDateTime now = LocalDateTime.now();
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                String formattedDateTime = now.format(formatter);

                System.out.println("当前时间: " + formattedDateTime);
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // System.out.println("Fixed Delay Task at " + System.currentTimeMillis());
        };

        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedDateTime = now.format(formatter);
        System.out.println("开始任务: " + formattedDateTime);

        // scheduler.scheduleWithFixedDelay(task, 1, 3, TimeUnit.SECONDS);
        scheduler.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);

        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        scheduler.shutdown();
    }

模拟实现定时器

1.创建一个类,表示一个任务

任务中含有开始时间以及是什么任务

java 复制代码
class MyTimerTask2 {
    // 持有成员的方式
    private Runnable task;
    private long time;
    public MyTimerTask2 (Runnable task,long time) {
        this.task = task;
        this.time = time;
    }
}

2.管理多个任务

由于定时是在某一个未来的时间的进行任务的开启,那么可以使用优先级队列对任务的时间按照任务的时间进行排序,那么就需要比较规则。这里是按照执行时间的先后顺序作为比较规则,需求是能够时间最小的元素能够在队首(小根堆)可以得到如下代码:

java 复制代码
private PriorityQueue<MyTimerTask2> queue = new PriorityQueue<>();
@Override
    public int compareTo(MyTimerTask2 o) {
        // 负数:this.time < o.time
        // 0  :this.time = o.time
        // 正数:this.time > o.time
        return Long.compare(this.time,o.time);
    }

3.实现schedule方法,把任务添加到队列

java 复制代码
public void schedule(Runnable task,long delay) {
        // 不是阻塞队列,不能用put
        MyTimerTask2 timerTask2 = new MyTimerTask2(task,System.currentTimeMillis()+delay);
        queue.offer(timerTask2);
    }

4.额外创建一个线程,负责执行队列中的任务

和线程池不同,线程池是只要队列不空,就立即取任务并执行,此处需要看队首元素的时间,时间到了才能执行。

上述的准备工作完成,现在开始创建一个线程去执行任务,如果队列不空,取出队首元素,如果当前任务时间大于系统时间,表明时机未到,不进行执行以及弹出队列。代码如下:

java 复制代码
public MyTimer() {
        // 创建线程,负责执行队列中的任务
        Thread t = new Thread(()->{
            while(true) {
                // 取队首元素
                if(!queue.isEmpty()) {
                    continue;
                }
                MyTimerTask2 task2 = queue.peek();
                if(task2.getTime() > System.currentTimeMillis()) {
                    // 当前任务时间大于比系统时间大,执行时机未到
                    continue;
                }else {
                    task2.run();
                    queue.poll();
                }
            }
        });
        t.start();
    }


public long getTime() {
        return time;
    }

    public void run() {
        task.run();
    }

但是当前调用schdule是一个线程,定时器内部又有一个线程,多线程操作一个队列,涉及到了线程安全问题,因此要加锁,对任务进队列和定时器内部进行加锁;

但是可能有关疑问,MyTimer()不是构造方法吗,构造方法本身可以synchronized,但是这里不能这样子写,要保护的逻辑在run里面,和构造方法不是同一个方法

如果此时执行,那么cpu就会高速运转,导致这个情况的原因是下面的代码:

队列空之后,一直在忙等,但是消耗大量CPU资源,为了避免这种情况,可以使用wait 和notify 机制来让线程在队列为空时进入等待状态,而不是忙等

相关推荐
侠客行03178 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪8 小时前
深入浅出LangChain4J
java·langchain·llm
灰子学技术9 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
老毛肚9 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎10 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
二十雨辰10 小时前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码10 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚10 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂10 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
pas13610 小时前
41-parse的实现原理&有限状态机
开发语言·前端·javascript