You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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,更可靠。

二、用ScheduledExecutorService强制单线程

要保证任务单线程执行,直接用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;
});

这样创建的调度器也是单线程的,而且线程属性更可控。

总结
  1. 给枚举单例加一个线程安全的初始化标记,确保startThread()只执行一次;
  2. 优先用ServletContextListener在应用启动时初始化,比在JSP里触发更可靠;
  3. 用Executors.newSingleThreadScheduledExecutor()或自定义单线程的ScheduledThreadPoolExecutor,保证任务单线程执行;
  4. 应用停止时记得关闭调度器,避免资源泄漏。

内容的提问来源于stack exchange,提问作者Cheknov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:20:59