如何用ScheduledExecutorService创建单线程并实现welcome.jsp代码仅执行一次
嘿,我来帮你搞定这两个问题!先聊聊怎么让那段初始化代码只跑一次,再说说ScheduledExecutorService的单线程控制~
首先得提一句:直接在JSP里写这段初始化代码其实不是最佳实践,因为JSP每次被请求时都会执行脚本里的代码(哪怕页面缓存了,也不是100%可靠)。咱们换更稳妥的方式来实现“只执行一次”的需求。
1. 修正你的枚举单例
你说用枚举单例没成功,大概率不是单例本身的问题——枚举单例是Java里最靠谱的单例实现之一,线程安全还能防止序列化破坏。问题应该出在你每次访问JSP都调用startThread(),哪怕实例是唯一的,方法还是会被反复触发。所以得在单例里加个“开关”,确保这个方法只执行一次:
public enum WeeklyScheduledMail { INSTANCE; // 用volatile保证多线程下的可见性 private volatile boolean isThreadStarted = false; private ScheduledExecutorService scheduler; public void startThread() { // 双重检查锁定,线程安全且只执行一次初始化 if (!isThreadStarted) { synchronized (WeeklyScheduledMail.class) { if (!isThreadStarted) { // 这里初始化你的调度器和任务 scheduler = Executors.newSingleThreadScheduledExecutor(); // 举个例子:每周执行一次任务,替换成你的业务逻辑 scheduler.scheduleAtFixedRate(() -> { // 你的邮件发送逻辑 System.out.println("执行每周邮件调度任务"); }, 0, 7, TimeUnit.DAYS); isThreadStarted = true; } } } } // 应用停止时记得关闭调度器,避免资源泄漏 public void stopThread() { if (scheduler != null && !scheduler.isShutdown()) { scheduler.shutdown(); } } }
现在不管你调用多少次startThread(),里面的初始化逻辑只会跑一次。如果一定要在JSP里触发,那只需要在welcome.jsp里保留<%WeeklyScheduledMail.INSTANCE.startThread(); %>就行,后续访问不会再执行核心逻辑。
2. 更优雅的方案:用ServletContextListener在应用启动时执行
如果你的需求是“服务器启动后,只要有用户访问过页面就执行一次”,其实更合适的是在应用启动时就自动执行,不需要等用户访问JSP。用ServletContextListener可以完美实现:
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @WebListener public class AppStartupListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { // 应用加载完成后立即执行,只跑一次 WeeklyScheduledMail.INSTANCE.startThread(); } @Override public void contextDestroyed(ServletContextEvent sce) { // 应用停止时关闭调度器,释放资源 WeeklyScheduledMail.INSTANCE.stopThread(); } }
这样服务器一启动,应用初始化完成后就会启动你的调度线程,完全不需要依赖用户访问JSP,更可靠。
要保证任务单线程执行,直接用Executors.newSingleThreadScheduledExecutor()就可以了——这个工厂方法会创建一个只有一个工作线程的调度器,所有提交的任务都会按顺序执行,不会出现多线程并发的情况。
需要注意两点:
- 一定要通过单例(比如上面的枚举)来管理这个调度器实例,避免创建多个调度器导致多线程;
- 应用停止时必须调用
scheduler.shutdown(),不然线程会一直占用资源,导致应用无法正常退出。
如果你想自定义线程的属性(比如给线程起名字、设置为守护线程),也可以手动创建调度器:
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1, r -> { Thread thread = new Thread(r); thread.setName("weekly-mail-scheduler"); // 给线程起个名字,方便排查问题 thread.setDaemon(true); // 可选:设置为守护线程,应用停止时自动退出 return thread; });
这样创建的调度器也是单线程的,而且线程属性更可控。
- 给枚举单例加一个线程安全的初始化标记,确保
startThread()只执行一次; - 优先用ServletContextListener在应用启动时初始化,比在JSP里触发更可靠;
- 用
Executors.newSingleThreadScheduledExecutor()或自定义单线程的ScheduledThreadPoolExecutor,保证任务单线程执行; - 应用停止时记得关闭调度器,避免资源泄漏。
内容的提问来源于stack exchange,提问作者Cheknov

