分布式定时任务系列12:XXL-job的任务触发为什么是死循环?
传送门分布式定时任务系列1XXL-job安装分布式定时任务系列2XXL-job使用分布式定时任务系列3任务执行引擎设计分布式定时任务系列4任务执行引擎设计续分布式定时任务系列5XXL-job中blockingQueue的应用分布式定时任务系列6XXL-job触发日志过大引发的CPU告警分布式定时任务系列7XXL-job源码分析之任务触发分布式定时任务系列8XXL-job源码分析之远程调用分布式定时任务系列9XXL-job路由策略分布式定时任务系列10XXL-job源码分析之路由策略番外篇从XXL-job路由策略的“服务容错“说起分布式定时任务系列13死循环是任务触发的银弹分布式定时任务系列14XXL-job的注册模型Java并发编程实战1java中的阻塞队列第一个问题XXL-job是如何做到定时触发的不知道大家在看XXL-job源码的过程中有没有像我一样产生过一个疑惑那就是XXL-job到底是怎样做到让任务按时触发的呢或者说让任务定时定点如此听话比如说一个邮件提醒功能每天晚上20:00点给相关的值班人员发邮件它到时间晚上20:00一定会触发吗会不会漏会不会判断不准超过时间21:00才触发触发整体时序图在分布式定时任务系列7XXL-job源码分析之任务触发节中从整体上梳理过任务触发的调用逻辑通过一个时序图来展现上面图中圈起来的地方立即创建一个线程Thread并启动不停扫描任务表xxl_job_info根据配置判断是否触发任务对应的代码如下我把核心的地方贴出来了{ // 立即创建一个线程 scheduleThread new Thread(new Runnable() { Override public void run() { try { TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); } catch (InterruptedException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } logger.info( init xxl-job admin scheduler success.); // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps 1000/50 20) int preReadCount (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20; // 不停扫描任务表xxl_job_info相当于线程的自旋 while (!scheduleThreadToStop) { // Scan Job long start System.currentTimeMillis(); Connection conn null; Boolean connAutoCommit null; PreparedStatement preparedStatement null; boolean preReadSuc true; try { // JDBC操作数据库 conn XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection(); connAutoCommit conn.getAutoCommit(); conn.setAutoCommit(false); // 加上db悲观锁防止并发执行 preparedStatement conn.prepareStatement( select * from xxl_job_lock where lock_name schedule_lock for update ); preparedStatement.execute(); // tx start // 1、pre read long nowTime System.currentTimeMillis(); // 查询所有任务列表一次最多6000个 ListXxlJobInfo scheduleList XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime PRE_READ_MS, preReadCount); if (scheduleList!null scheduleList.size()0) { // 2、push time-ring for (XxlJobInfo jobInfo: scheduleList) { // time-ring jump if (nowTime jobInfo.getTriggerNextTime() PRE_READ_MS) { // 2.1、trigger-expire 5spass make next-trigger-time logger.warn( xxl-job, schedule misfire, jobId jobInfo.getId()); // 1、错过触发时间根据策略决定是否立即补尝执行一次 MisfireStrategyEnum misfireStrategyEnum MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); if (MisfireStrategyEnum.FIRE_ONCE_NOW misfireStrategyEnum) { // FIRE_ONCE_NOW 》 trigger JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null); logger.debug( xxl-job, schedule push trigger : jobId jobInfo.getId() ); } // 2、更新下次执行相关时间参数 refreshNextValidTime(jobInfo, new Date()); } else if (nowTime jobInfo.getTriggerNextTime()) { // 2.2、trigger-expire 5sdirect-trigger make next-trigger-time // 1、触发任务 JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); logger.debug( xxl-job, schedule push trigger : jobId jobInfo.getId() ); // 2、更新下次执行相关时间参数 refreshNextValidTime(jobInfo, new Date()); // next-trigger-time in 5s, pre-read again if (jobInfo.getTriggerStatus()1 nowTime PRE_READ_MS jobInfo.getTriggerNextTime()) { // 1、make ring second int ringSecond (int)((jobInfo.getTriggerNextTime()/1000)%60); // 2、push time ring pushTimeRing(ringSecond, jobInfo.getId()); // 3、fresh next refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); } } else { // 2.3、trigger-pre-readtime-ring trigger make next-trigger-time // 1、make ring second int ringSecond (int)((jobInfo.getTriggerNextTime()/1000)%60); // 2、push time ring pushTimeRing(ringSecond, jobInfo.getId()); // 3、fresh next refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); } } // 3、更新db表中下次执行相关时间参数 for (XxlJobInfo jobInfo: scheduleList) { XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo); } } else { preReadSuc false; } // tx stop } catch (Exception e) { if (!scheduleThreadToStop) { logger.error( xxl-job, JobScheduleHelper#scheduleThread error:{}, e); } } finally { // commit if (conn ! null) { try { conn.commit(); } catch (SQLException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } try { conn.setAutoCommit(connAutoCommit); } catch (SQLException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } try { conn.close(); } catch (SQLException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } } // close PreparedStatement if (null ! preparedStatement) { try { preparedStatement.close(); } catch (SQLException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } } } long cost System.currentTimeMillis()-start; // Wait seconds, align second if (cost 1000) { // scan-overtime, not wait try { // pre-read period: success scan each second; fail skip this period; TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000); } catch (InterruptedException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } } } logger.info( xxl-job, JobScheduleHelper#scheduleThread stop); } }); // 线程启动 scheduleThread.setDaemon(true); scheduleThread.setName(xxl-job, admin JobScheduleHelper#scheduleThread); scheduleThread.start(); }这个JobScheduleHelper逻辑有多个分支但是里面核心是立即创建一个线程这里是单线程在run()方法中通过一个while循环来触发任务。但是这个while关键的一点是如果线程没有停止就会一直执行下去// 不停扫描任务表xxl_job_info相当于线程的自旋 while (!scheduleThreadToStop) { // 执行任务策略并触好,具体见上这里略过... }我们都知道Java里面的线程有可能阻塞或者由于CPU切换得不到时间片分配导致任务暂停这些情况下任务不就触发不了么所以XXL-job在run()方法里面写了类似死循环来尽量避免任务延迟触发这一点其实在其它定时任务设计中或框架也是一种通用思路所以这次会通过JDK自带的Timer工具类源码来印证、对比、扩展JDK-Timer类源码解析JDK-Timer是什么‌JDK Timer‌是Java开发工具包JDK中提供的一个定时器工具用于在后台线程中安排将来执行的任务。它可以安排任务为一次性执行或者以固定间隔重复执行。Timer类自jdk1.3就引入作者是大名鼎鼎的Josh Bloch是google的首席JAVA架构师。既然它开始工作的早那么退休自然也早现在一般不再推荐使用优点‌简单易用适合简单的定时任务需求。线程安全多个线程可以共享一个Timer对象而无需外部同步。‌缺点‌单一Timer对象共享一个线程如果任务执行时间较长会影响后续任务的执行。不适合高并发或高性能要求的场景。‌替代方案‌‌ScheduledExecutorService‌提供了更强大的功能包括线程池支持、更灵活的任务调度等。例如可以使用scheduleAtFixedRate和scheduleWithFixedDelay方法来安排任务的固定频率执行。‌Quartz Scheduler‌一个更强大的调度框架支持复杂的调度需求、持久化存储、集群等功能适合企业级应用。但是通过它可以看看任务调度的原理以及学习一下大师的设计JDK-Timer的使用Timer的API方法并不多主要有以下几个变量和类型方法描述voidcancel()终止此计时器丢弃当前计划的任何任务。intpurge()从此计时器的任务队列中删除所有已取消的任务。voidschedule​(TimerTask task, long delay)在指定的延迟后安排指定的任务执行。voidschedule​(TimerTask task, long delay, long period)在指定的延迟之后开始为重复的固定延迟执行安排指定的任务。voidschedule​(TimerTask task, Date time)计划在指定时间执行指定的任务。voidschedule​(TimerTask task, Date firstTime, long period)从指定时间开始为重复的固定延迟执行安排指定的任务。voidscheduleAtFixedRate​(TimerTask task, long delay, long period)在指定的延迟之后开始为重复的固定速率执行安排指定的任务。voidscheduleAtFixedRate​(TimerTask task, Date firstTime, long period)从指定时间开始为重复的固定速率执行安排指定的任务。下面通过几个测试方法来感受一下public class TimerTest { public static void main(String[] args) { // 通过new实例化一个timer实例 Timer timer new Timer(); // 只触发一次的任务:延迟1s执行(如果为0则表示立即执行) timer.schedule(new TimerTask() { Override public void run() { System.out.println(name1 Thread.currentThread().getName() , time: new Date()); } }, 1000L); // 固定频率执行的任务:延迟1s执行,每2秒执行一次 TimerTask timerTask2 new TimerTask() { Override public void run() { System.out.println(name2 Thread.currentThread().getName() , time: new Date()); } }; timer.schedule(timerTask2, 1000L, 2000L); // 固定频率执行的任务:延迟1s执行,每3秒执行一次;并指定首次执行的时间 Date fixedDate getFixedDate(); timer.schedule(new TimerTask() { Override public void run() { System.out.println(name3 Thread.currentThread().getName() , time: new Date()); } }, fixedDate, 3000L); } public static Date getFixedDate() { String dateString 2025-06-08 19:19:59; SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); try { Date specifiedDate sdf.parse(dateString); System.out.println(Specified Date: specifiedDate); return specifiedDate; } catch (Exception e) { System.out.println(Error parsing date: e.getMessage()); } return null; } }运行输出结果Specified Date: Sun Jun 08 21:53:59 CST 2025 // name1输出在下面日志中可以看出只触发了一次属于一次性任务 name1Timer-0, time:Sun Jun 08 21:53:48 CST 2025 // name2输出在下面日志中可以每隔2s触发一次属于固定频率任务 name2Timer-0, time:Sun Jun 08 21:53:48 CST 2025 name2Timer-0, time:Sun Jun 08 21:53:50 CST 2025 name2Timer-0, time:Sun Jun 08 21:53:52 CST 2025 name2Timer-0, time:Sun Jun 08 21:53:54 CST 2025 name2Timer-0, time:Sun Jun 08 21:53:56 CST 2025 name2Timer-0, time:Sun Jun 08 21:53:58 CST 2025 // name3输出表示在指定的时间节点才触发了任务属于固定频率任务 name3Timer-0, time:Sun Jun 08 21:53:59 CST 2025 name2Timer-0, time:Sun Jun 08 21:54:00 CST 2025 name3Timer-0, time:Sun Jun 08 21:54:02 CST 2025 name2Timer-0, time:Sun Jun 08 21:54:02 CST 2025 name2Timer-0, time:Sun Jun 08 21:54:04 CST 2025 name3Timer-0, time:Sun Jun 08 21:54:05 CST 2025除去上述几个API之外还有scheduleAtFixedRate相关的几个方法也是设置固定频率执行的操作区别是scheduleAtFixedRate更注重任务执行频率如果任务由于其它原因被阻塞当恢复时会尽力去补偿执行遗漏的schedule这种更注重任务执行时间如果任务由于其它原因被阻塞当恢复时不会去补偿执行遗漏的可以通过如下例子来感受public static void main(String[] args) { // 通过new实例化一个timer实例 Timer timer new Timer(); // 只触发一次的任务:延迟1s执行(如果为0则表示立即执行) timer.schedule(new TimerTask() { Override public void run() { System.out.println(name1 Thread.currentThread().getName() , time: new Date()); try { // 模拟任务阻塞线程睡眠6s Thread.sleep(6000, TimeUnit.SECONDS.ordinal()); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, 1000L); timer.scheduleAtFixedRate(new TimerTask() { Override public void run() { System.out.println(name4 Thread.currentThread().getName() , time: new Date()); } }, 1000L, 2000L); } // name1输出在下面日志中可以看出只触发了一次属于一次性任务 name1Timer-0, time:Sun Jun 08 22:31:18 CST 2025 // name4输出在下面几个name4的连续输出中可以看到是同一时刻执行的这就是由于任务1中线程阻塞了6s所以被补偿执行了 name4Timer-0, time:Sun Jun 08 22:31:24 CST 2025 name4Timer-0, time:Sun Jun 08 22:31:24 CST 2025 name4Timer-0, time:Sun Jun 08 22:31:24 CST 2025 name4Timer-0, time:Sun Jun 08 22:31:24 CST 2025 // name4输出后面都是每隔2s的正常输出了 name4Timer-0, time:Sun Jun 08 22:31:26 CST 2025 name4Timer-0, time:Sun Jun 08 22:31:28 CST 2025 name4Timer-0, time:Sun Jun 08 22:31:30 CST 2025JDK-Timer的源码从构造方法看类结构先看下Timer的创建// 通过new实例化一个timer实例 Timer timer new Timer();跟进Timer的类的源码看看这个构造方法/** * Creates a new timer. The associated thread does inot/i * {linkplain Thread#setDaemon run as a daemon}. */ public Timer() { this(Timer- serialNumber()); } /** * Creates a new timer whose associated thread has the specified name. * The associated thread does inot/i * {linkplain Thread#setDaemon run as a daemon}. * * param name the name of the associated thread * throws NullPointerException if {code name} is null * since 1.5 */ public Timer(String name) { thread.setName(name); thread.start(); }首先Timer()无参构造方法调用会继续调用Timer(String name)这个有参构造传递一个线程的名称名称为Timer-序列号/** * This ID is used to generate thread names. */ private final static AtomicInteger nextSerialNumber new AtomicInteger(0); // 上面的原子类注释说明了nextSerialNumber是生成一个线程名称 private static int serialNumber() { return nextSerialNumber.getAndIncrement(); }那么这个线程到底是什么呢TimerThread-定时器线程我们在前面说过Timer是单线程的其实就是在这里绑定的继续看下thread定义/** * The timer task queue. This data structure is shared with the timer * thread. The timer produces tasks, via its various schedule calls, * and the timer thread consumes, executing timer tasks as appropriate, * and removing them from the queue when theyre obsolete. */ private final TaskQueue queue new TaskQueue(); /** * The timer thread. */ private final TimerThread thread new TimerThread(queue);这里创建了一个TimerThread对象得到一个实例线程class TimerThread extends Thread { /** * This flag is set to false by the reaper to inform us that there * are no more live references to our Timer object. Once this flag * is true and there are no more tasks in our queue, there is no * work left for us to do, so we terminate gracefully. Note that * this field is protected by queues monitor! */ boolean newTasksMayBeScheduled true; /** * Our Timers queue. We store this reference in preference to * a reference to the Timer so the reference graph remains acyclic. * Otherwise, the Timer would never be garbage-collected and this * thread would never go away. */ private TaskQueue queue; TimerThread(TaskQueue queue) { this.queue queue; } public void run() { try { mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled false; queue.clear(); // Eliminate obsolete references } } } /** * The main timer loop. (See class comment.) */ private void mainLoop() { // 实现暂时略后面会详细讨论 } }这里创建了一个TimerThread对象得到一个实例线程在学习Java并发编程的时候肯定学习过线程创建的2种方式继承Thread或者实现Runnable接口这里就是典型的继续Thread类TaskQueue-定时器任务队列除此以外在创建Timer实例的时候还实例化了一个队列// 定时器任务队列。这一数据结构与定时器线程共享。 //定时器通过不同的调度调用生成任务定时器线程则负责消费这些任务在适当时机执行它们并在任务过期后从队列中移除。 private final TaskQueue queue new TaskQueue();从上面可以得到它的主要作用数据结构共享队列由定时器和定时器线程共同操作。生产者-消费者模型定时器生产者创建任务定时器线程消费者处理任务。任务生命周期执行后或过期时任务会被移出队列。继续跟进定时器任务队列的类定义class TaskQueue { // 任务数组是一个最少堆结构 private TimerTask[] queue new TimerTask[128]; }里面定义了一个TimerTask数组定义了定时任务TimerTask-定时任务// 定义了一个抽象类实现了Runnable接口 public abstract class TimerTask implements Runnable { // 定义了一个抽象方法定时任务要执行业务方法就是通过实现这个方法 public abstract void run(); // 其余参数先略过... }至此Timer的相关类已经都出现了可以据此得到它的类图类图至此得到了Timer类的完整类图其中Timer是定时器主类持有一个执行线程TimerThread和一个任务队列TaskQueueTimerThread类负责执行定时任务引用了任务队列TaskQueueTaskQueue类为任务队列里面持有一个TimerTask数组也是最小堆结构TimerTask为业务要实现的定时任务接口抽象类它实现Runable接口所以自然要求实现对应的run()方法接下来就仔细分析一下定时任务的整个管理、执行流程定时器-任务线程启动再回到刚才的构造方法来public Timer(String name) { thread.setName(name); // 线程启动 thread.start(); }只要创建了一个Timer实例new Timer()就会立即启动执行线程thread.start定时任务执行线程启动之后会立即执行线程的run方法public void run() { try { // 主循环所有的定时任务都是在这里触发执行的 mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled false; queue.clear(); // Eliminate obsolete references } } }从方法命名上就可以看出这个mainLoop是一个循环方法继续跟进它private void mainLoop() { // 死循环 while (true) { try { // 当前需要触发的定时任务 TimerTask task; // 触发标志位 boolean taskFired; // 直接同步任务队列防止并发 synchronized(queue) { // Wait for queue to become non-empty // 如果队列为空且Timer未取消则进行等待关于Timer取消造成newTasksMayBeScheduled为false的放到后面讨论 while (queue.isEmpty() newTasksMayBeScheduled) queue.wait(); // 如果队列为空且Timer取消主动或被动则跳出循环注意只有这种情况才会真正结束死循环其实就是newTasksMayBeScheduled为false了 if (queue.isEmpty()) break; // Queue is empty and will forever remain; die // Queue nonempty; look at first evt and do the right thing long currentTime, executionTime; // 获取第一个任务 task queue.getMin(); // 同步任务也是防止并发 synchronized(task.lock) { // 如果任务取消虽从任务队列中删除任务 if (task.state TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } currentTime System.currentTimeMillis(); executionTime task.nextExecutionTime; // 判断任务是否需要触发触发条件是执行时间在当前时间之前即executionTimecurrentTime if (taskFired (executionTimecurrentTime)) { // 如果是一次性任务 if (task.period 0) { // Non-repeating, remove // 则从任务队列中删除任务 queue.removeMin(); // 修改任务的状态为已执行 task.state TimerTask.EXECUTED; // 如果是周期性任务 } else { // Repeating task, reschedule // 则重新计算下一次触发时间 queue.rescheduleMin( task.period0 ? currentTime - task.period : executionTime task.period); } } } // 如果任务未触发则继续等待时间executionTime - currentTime if (!taskFired) // Task hasnt yet fired; wait queue.wait(executionTime - currentTime); } // 如果任务触发则调用run()方法执行任务 if (taskFired) // Task fired; run it, holding no locks task.run(); } catch(InterruptedException e) { } } }从第一行代码就看到了我们今天引出的问题为什么定时任务的触发是死循环看来不止是XXL-job这样实现的JDK-Timer也是这样实现的while (true) { }关于这个问题网上AI给出的一种解释任务调度原理采用死循环设计的原因‌在于其能够有效地管理和执行多任务提高系统的整体效率和响应速度。在任务调度中每个任务通常设计为一个死循环这是因为死循环结构能够确保任务持续运行并等待事件或条件的发生从而实现对任务的持续监控和执行。死循环在任务调度中的作用‌持续运行‌死循环确保任务能够持续运行不断检查和处理事件或条件。这对于实时响应和任务管理至关重要‌。‌事件驱动‌死循环结构使得任务能够响应各种事件或信号如定时器中断、外部事件等。当特定事件发生时任务可以执行相应的操作‌。‌资源分配‌通过维护就绪队列和数据结构调度程序能够动态选择待执行任务实现处理器资源的有效分配和管理‌定时任务添加关于定时任务执行主流程源码的讨论先点到为止。回过头来再看看定时器添加任务的例子// 只触发一次的任务:延迟1s执行(如果为0则表示立即执行) timer.schedule(new TimerTask() { Override public void run() { System.out.println(name1 Thread.currentThread().getName() , time: new Date()); } }, 1000L);继续跟进schedule方法public void schedule(TimerTask task, long delay) { // 延迟时间要0意思就是任务只能当前时间之后这点很容易理解 if (delay 0) throw new IllegalArgumentException(Negative delay.); // 从这里就可以看出delay0原因因为执行时间直接取的是 当前时间延迟时间 sched(task, System.currentTimeMillis()delay, 0); } private void sched(TimerTask task, long time, long period) { // 任务执行时间 if (time 0) throw new IllegalArgumentException(Illegal execution time.); // Constrain value of period sufficiently to prevent numeric // overflow while still being effectively infinitely large. if (Math.abs(period) (Long.MAX_VALUE 1)) period 1; // 同步队列 synchronized(queue) { // 如果定时器被取消直接抛出异常 if (!thread.newTasksMayBeScheduled) throw new IllegalStateException(Timer already cancelled.); // 同步任务 synchronized(task.lock) { if (task.state ! TimerTask.VIRGIN) throw new IllegalStateException( Task already scheduled or cancelled); // 更新任务信息设置下次执行时间、周期、任务状态为SCHEDULED在这个状态的任务才能被执行这个可以在上面执行器中代码可以印证 task.nextExecutionTime time; task.period period; task.state TimerTask.SCHEDULED; } // 添加到任务队列中 queue.add(task); // 如果任务恰好是队列第一个元素则直接通知任务队列执行任务 if (queue.getMin() task) // 这个代码还有另外一个作用就是唤醒被阻塞的队列因为在执行的代码中如果队列为空会进行等待的。 queue.notify(); } }任务执行流程图结合上面的任务执行/添加源码分析在不考虑主动取消定时器的情况可以大致得出下面的任务执行流程图生产者-消费者模型至此梳理了任务执行的流程留意一下可以看到里面用到了wait()notify()方法用来做线程的阻塞与唤醒这其实是典型的生产者-消费者模型生产者业务类通过调用Timer的schedule方法添加任务TimerTask其实就是生产者比如上面的TimerTest消费者这里的执行线程TimerThread其实就是消费者它通过while循环不停的从任务队列中取出定时任务但是这里要明确一下执行线程既是消费者也是生产者!可以从mainLoop()的方法可以体现if (taskFired (executionTimecurrentTime)) { // 如果是一次性任务 if (task.period 0) { // Non-repeating, remove // 则从任务队列中删除任务 queue.removeMin(); // 修改任务的状态为已执行 task.state TimerTask.EXECUTED; // 如果是周期性任务 } else { // Repeating task, reschedule // 则重新计算下一次触发时间 queue.rescheduleMin( task.period0 ? currentTime - task.period : executionTime task.period); } }如果任务的周期字段是period如果是0表示一次性任务如果period0则表示周期性任务在任务触发时同时计算下一次触发时间就是queue.rescheduleMin方法void rescheduleMin(long newTime) { // 更新下次执行时间当前时间周期 queue[1].nextExecutionTime newTime; // 调整堆顺序保持最小堆特性 fixDown(1); }通过这个方法其实相当于又往队列了放了一个元素就是同一个任务还在队列中只是下次执行时间nextExecutionTime被更新了这样在任务触发之后可以继续被执行关于复用wait和notify实现线程间的协作这里不展开。只是列出一下相关的APIvoidnotify()唤醒正在此对象监视器上等待的单个线程。voidnotifyAll()唤醒等待此对象监视器的所有线程。voidwait()导致当前线程等待它被唤醒通常是通知或中断。voidwait​(long timeoutMillis)导致当前线程等待它被唤醒通常是通知或中断或者直到经过一定量的实时。voidwait​(long timeoutMillis, int nanos)导致当前线程等待它被唤醒通常是通知或中断或者直到经过一定量的实时。最小堆前面提到过TaskQueue类为任务队列里面持有一个TimerTask数组也是最小堆结构关于最小堆的介绍在这里列出来经过排序的完全二叉树最小堆是一种经过排序的完全二叉树其中任一非终端节点的数据值均不大于其左子结点和右子结点的值。这里在添加任务的时候其实就进行堆排序void add(TimerTask task) { // Grow backing store if necessary if (size 1 queue.length) queue Arrays.copyOf(queue, 2*queue.length); queue[size] task; fixUp(size); } private void fixUp(int k) { while (k 1) { int j k 1; if (queue[j].nextExecutionTime queue[k].nextExecutionTime) break; TimerTask tmp queue[j]; queue[j] queue[k]; queue[k] tmp; k j; } }附录Java创建的2种方式1. 继承Thread类这是最传统的创建线程的方式。通过创建一个新的类继承自java.lang.Thread类并重写run()方法来定义线程的执行体。然后创建该类的实例并调用其start()方法来启动线程。2. 实现Runnable接口这种方式更加灵活因为它允许你将线程的执行代码封装在一个实现了Runnable接口的类的实例中。这种方式更适合资源共享和便于线程的共享和管理。

相关新闻

Leetcode排序算法面试核心:快排/归并/堆排实战变形与工程落地

Leetcode排序算法面试核心:快排/归并/堆排实战变形与工程落地

1. 为什么刷排序算法是Leetcode和面试绕不开的“硬门槛”你打开Leetcode,点开“Top Interview Questions”列表,前20题里至少有3道直接考排序变体;你参加某大厂后端岗初面,面试官没问你八股文,第一句就是:“…

2026/10/17 14:18:39 阅读更多 →
LeetCode 20. Valid Parentheses:一道栈题的完整剖析——从算法原理到十种语言实现

LeetCode 20. Valid Parentheses:一道栈题的完整剖析——从算法原理到十种语言实现

示例工程 【免费下载链接】leetcode 🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解 项目地址: https://git…

2026/10/17 23:27:42 阅读更多 →
SpringBoot+Vue+MySQL协同过滤推荐系统实战:东北特产电商毕设全拆解

SpringBoot+Vue+MySQL协同过滤推荐系统实战:东北特产电商毕设全拆解

带了几届做电商类毕设的学生之后,我发现"推荐系统"几乎是所有销售平台题目的标配,但大部分同学停留在"跑通一个Demo"的程度,论文里写得花团锦簇,一问细节就露馅。其中"SpringBoot Vue MySQL 协同过滤…

2026/10/16 5:38:38 阅读更多 →

最新新闻

Cursor 编辑器从入门到高效使用:AI 代码补全与项目实操指南

Cursor 编辑器从入门到高效使用:AI 代码补全与项目实操指南

我最初是从一次毫无准备的试用开始的。同事把编辑器快捷键发给我,说了一句“你试试直接按 Tab”,然后我的光标就自动补完了一整段文件读取代码。那一刻我意识到,这已经不是旧时代那个只会补全关键字拼写的“智能输入法”了,而是一…

2026/10/18 18:29:59 阅读更多 →
《2025企业级AI应用报告》深度拆解:从模型竞赛到业务落地的完整路径与避坑指南

《2025企业级AI应用报告》深度拆解:从模型竞赛到业务落地的完整路径与避坑指南

《2025年国内企业级AI应用行业研究报告》出来之后,我身边做企业服务、数字化转型的同行基本都在转。说实话,市面上AI分析内容不少,但大多数停留在模型参数、榜单评测、消费级玩法,真正把“企业级应用”讲透的很少。这份报告把视角…

2026/10/18 18:29:58 阅读更多 →
瑞萨RA6M5+Qt遥控小车:FSP配置、PWM驱动与串口协议实战

瑞萨RA6M5+Qt遥控小车:FSP配置、PWM驱动与串口协议实战

简介:面向嵌入式与物联网入门者的瑞萨RA6M5遥控小车完整工程,整合FSP图形化配置、Keil开发环境与Qt Android跨端联调。整套方案利用板载电容按键一键启动,触发ESP8266无线模块初始化;手机端应用开启服务后,小车即可上报…

2026/10/18 18:29:58 阅读更多 →
链表高频面试题:双指针一次遍历求倒数第k个节点

链表高频面试题:双指针一次遍历求倒数第k个节点

1. 从一道高频面试题说起“面试题.返回倒数第k个节点”这行标题,过去几年我见过太多次了——不管是校招还是社招,不管是后端岗还是客户端岗,这道题出现的频率高得离谱。它考察的知识点不算难,却能把一个人的基本功底子摸得清清楚楚…

2026/10/18 18:29:58 阅读更多 →
BrowserSkill:用CLI驱动Chrome的浏览器自动化工具,让AI Agent轻松操控浏览器

BrowserSkill:用CLI驱动Chrome的浏览器自动化工具,让AI Agent轻松操控浏览器

1. 从命令行到浏览器:BrowserSkill 到底在解决什么问题第一次看到 BrowserSkill 这个名字,我脑子里蹦出来的画面是:一个跑在终端里的命令行工具,能直接操控 Chrome 浏览器,把网页打开、点击、填表、截图、抓数据这一整…

2026/10/18 18:29:58 阅读更多 →
SolidWorks本地AI插件:DeepSeek-R1驱动的工程智能体

SolidWorks本地AI插件:DeepSeek-R1驱动的工程智能体

简介:本资源是面向SolidWorks中高级用户与二次开发工程师的DeepSeek智能插件项目源码包,旨在解决CAD设计过程中建模效率低、参数优化依赖人工等痛点,支持智能建模、AI辅助参数调优等高阶功能。压缩包仅4KB,含3个核心文件&#xff…

2026/10/18 18:28:58 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →