Java Web中能否让一个ScheduledExecutorService执行完成后再启动另一个?
如何实现应用启动时调度服务按顺序执行(先完成第一个再启动第二个)
当然可以实现!不过你当前的方案里,两个ServletContextListener会被容器并行初始化,而且各自的调度任务是异步启动的,没法保证执行顺序。下面给你两种靠谱的解决方案:
方案一:合并为一个Listener,手动控制执行顺序
最稳妥的方式是把两个调度逻辑合并到同一个ServletContextListener里,用单线程调度池保证任务串行,先同步执行第一个任务的首次运行,完成后再启动第二个任务的调度。
示例代码如下:
public class OrderedScheduler implements ServletContextListener { private ScheduledExecutorService executorService = null; @Override public void contextInitialized(ServletContextEvent sce) { // 使用单线程调度池,确保任务串行执行 executorService = Executors.newSingleThreadScheduledExecutor(); // 1. 先同步执行第一个任务的首次运行,等待它完成 FirstScheduledTask firstTask = new FirstScheduledTask(); firstTask.run(); // 2. 启动第一个任务的定期调度(从15天后开始重复执行) executorService.scheduleAtFixedRate(firstTask, 15, 15, TimeUnit.DAYS); // 3. 现在启动第二个任务的调度(可以选择先同步执行一次,或者直接启动定期调度) SecondScheduledTask secondTask = new SecondScheduledTask(); // 如果需要第二个任务也先执行一次再进入定期调度,取消下面的注释 // secondTask.run(); executorService.scheduleAtFixedRate(secondTask, 0, 15, TimeUnit.DAYS); } @Override public void contextDestroyed(ServletContextEvent sce) { if (executorService != null) { executorService.shutdown(); try { // 等待线程池关闭,超时则强制终止 if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { executorService.shutdownNow(); } } catch (InterruptedException e) { executorService.shutdownNow(); Thread.currentThread().interrupt(); } } } // 第一个调度任务 class FirstScheduledTask implements Runnable { private MyEjb ejbObject; public FirstScheduledTask() { // 初始化EJB try { ejbObject = (MyEjb) new InitialContext().lookup("java:global/project/MyEjb"); } catch (NamingException ex) { Logger.getLogger(OrderedScheduler.class.getName()).log(Level.SEVERE, "Failed to lookup First EJB", ex); } } @Override public void run() { // 第一个任务的业务逻辑 try { // 使用ejbObject执行操作 System.out.println("First scheduled task executed"); } catch (Exception e) { Logger.getLogger(OrderedScheduler.class.getName()).log(Level.SEVERE, "First task execution failed", e); } } } // 第二个调度任务 class SecondScheduledTask implements Runnable { private AnotherEjb anotherEjb; public SecondScheduledTask() { // 初始化第二个EJB try { anotherEjb = (AnotherEjb) new InitialContext().lookup("java:global/project/AnotherEjb"); } catch (NamingException ex) { Logger.getLogger(OrderedScheduler.class.getName()).log(Level.SEVERE, "Failed to lookup Second EJB", ex); } } @Override public void run() { // 第二个任务的业务逻辑 try { // 使用anotherEjb执行操作 System.out.println("Second scheduled task executed"); } catch (Exception e) { Logger.getLogger(OrderedScheduler.class.getName()).log(Level.SEVERE, "Second task execution failed", e); } } } }
这个方案的核心优势:
- 用单线程调度池从根源上避免并行执行的顺序问题
- 完全手动控制任务启动顺序,逻辑清晰,容易维护
- 统一管理线程池,减少资源泄漏风险
方案二:用同步工具控制两个Listener的执行顺序
如果不想合并Listener,可以用CountDownLatch这类同步工具,让第二个Listener等待第一个任务完成后再启动。不过这种方式依赖容器对Listener初始化顺序的支持(主流容器如Tomcat、WildFly都支持)。
第一个Listener(先执行的任务)
public class FirstScheduler implements ServletContextListener { private ScheduledExecutorService executorService; @Override public void contextInitialized(ServletContextEvent sce) { executorService = Executors.newSingleThreadScheduledExecutor(); // 创建一个倒计时锁存器,用于通知第二个Listener任务完成 CountDownLatch taskCompletedLatch = new CountDownLatch(1); sce.getServletContext().setAttribute("firstTaskCompletedLatch", taskCompletedLatch); FirstTask task = new FirstTask(); // 同步执行第一个任务 task.run(); // 标记任务完成 taskCompletedLatch.countDown(); // 启动定期调度 executorService.scheduleAtFixedRate(task, 15, 15, TimeUnit.DAYS); } @Override public void contextDestroyed(ServletContextEvent sce) { if (executorService != null) { executorService.shutdown(); } } class FirstTask implements Runnable { private MyEjb ejbObject; public FirstTask() { try { ejbObject = (MyEjb) new InitialContext().lookup("java:global/project/MyEjb"); } catch (NamingException ex) { Logger.getLogger(FirstScheduler.class.getName()).log(Level.SEVERE, null, ex); } } @Override public void run() { // 第一个任务的业务逻辑 } } }
第二个Listener(等待第一个任务完成)
public class SecondScheduler implements ServletContextListener { private ScheduledExecutorService executorService; @Override public void contextInitialized(ServletContextEvent sce) { // 获取第一个Listener设置的锁存器 CountDownLatch latch = (CountDownLatch) sce.getServletContext().getAttribute("firstTaskCompletedLatch"); if (latch != null) { try { // 等待第一个任务完成 latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Logger.getLogger(SecondScheduler.class.getName()).log(Level.SEVERE, "Interrupted while waiting for first task", e); return; } } // 启动第二个任务的调度 executorService = Executors.newSingleThreadScheduledExecutor(); SecondTask task = new SecondTask(); executorService.scheduleAtFixedRate(task, 0, 15, TimeUnit.DAYS); } @Override public void contextDestroyed(ServletContextEvent sce) { if (executorService != null) { executorService.shutdown(); } } class SecondTask implements Runnable { // 第二个任务的EJB初始化和业务逻辑 } }
⚠️ 注意:要在web.xml中把第一个Listener放在第二个前面,确保容器先初始化第一个Listener:
<listener> <listener-class>com.yourpackage.FirstScheduler</listener-class> </listener> <listener> <listener-class>com.yourpackage.SecondScheduler</listener-class> </listener>
关键注意事项
- 避免长时间阻塞应用启动:如果第一个任务执行时间很长,同步执行会导致应用启动变慢甚至超时。这种情况下可以改用异步执行,通过
Future获取任务执行结果,等待完成后再启动第二个任务。 - EJB初始化时机:有些服务器在Listener初始化时EJB可能还未就绪,建议把EJB的lookup逻辑放到任务的
run方法中,或者使用@PostConstruct注解确保EJB初始化完成。 - 线程池资源管理:一定要在
contextDestroyed方法中正确关闭线程池,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Diaboliko
相关产品推荐
相关产品推荐

