ScheduledExecutorService

引言

ScheduledExecutorService 是啥?

Java 的定时任务线程池。用它可以:

  • 延时 执行一次任务:schedule(...)

  • 按固定频率 循环执行:scheduleAtFixedRate(...)

  • 按固定间隔 循环执行:scheduleWithFixedDelay(...)

三个核心方法差异

  1. schedule(task, delay, unit)延时一次
  2. scheduleAtFixedRate(task, initialDelay, period, unit)按"时钟频率"跑;不等上一个任务结束的时间来对齐下一个开始时间(可能堆积)
  3. scheduleWithFixedDelay(task, initialDelay, delay, unit)上一个结束后再等 delay 才开始下一个(不堆积,更稳)

使用

java使用

java 复制代码
import java.util.concurrent.*;

ScheduledExecutorService scheduler =
    Executors.newScheduledThreadPool(1); // 常用:单线程定时器

// 1) 延时 2 秒执行一次
ScheduledFuture<?> oneShot = scheduler.schedule(() -> {
    System.out.println("do once");
}, 2, TimeUnit.SECONDS);

// 2) 固定频率:1 秒后启动,每 500ms 触发(可能堆积)
ScheduledFuture<?> fixedRate = scheduler.scheduleAtFixedRate(() -> {
    try {
        doWork();
    } catch (Exception e) {
        e.printStackTrace(); // 一定要捕获异常,否则周期任务会停止
    }
}, 1, 500, TimeUnit.MILLISECONDS);

// 3) 固定间隔:1 秒后启动,每次结束后隔 500ms 再下一次
ScheduledFuture<?> fixedDelay = scheduler.scheduleWithFixedDelay(() -> {
    try {
        doWork();
    } catch (Exception e) {
        e.printStackTrace();
    }
}, 1, 500, TimeUnit.MILLISECONDS);

// 取消任务
fixedRate.cancel(false); // 参数 true 则中断正在执行的线程

// 退出线程池
scheduler.shutdown(); // 或者 shutdownNow();

Kotlin(Android 常用)

Kotlin 复制代码
val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1)

// 延时一次
val future = scheduler.schedule({
    // do once
}, 2, TimeUnit.SECONDS)

// 固定间隔(更常用,避免堆积)
val periodic = scheduler.scheduleWithFixedDelay({
    try {
        // your periodic work
    } catch (t: Throwable) {
        t.printStackTrace()
    }
}, 1, 500, TimeUnit.MILLISECONDS)

// 取消
periodic.cancel(false)

// 关闭
scheduler.shutdown()

实践

  • 需要精准频率(如心跳/采样)scheduleAtFixedRate

  • 任务耗时不稳定/不能堆积scheduleWithFixedDelay(大多数业务更安全)

  • 只执行一次schedule

常见坑 & 最佳实践

  1. 一定捕获异常 :周期任务里抛出未捕获异常会让该周期任务直接停止。
  2. 不要长时间阻塞 定时线程:如果任务很慢,用线程池执行实际工作,定时器只负责"触发"。
  3. 合理线程数newScheduledThreadPool(1)足够多数场景;需要并行周期任务时再加大线程数。
  4. 取消要清理 :保存 ScheduledFuture<?>,在 onStop()/onDestroy()cancel(),再 shutdown()
  5. Android UI :它跑在后台线程,不能直接操作 UI ;需要切回主线程(Handler/runOnUiThread/LiveData/Flow)。
  6. Android 替代 :简单周期任务可用 Kotlin 协程
Kotlin 复制代码
scope.launch {
    delay(1000)
    while (isActive) {
        doWork()
        delay(500) // 等价于 fixedDelay
    }
}

需要约束(充电/Wi-Fi/重启恢复)请选择 WorkManager

项目里可以这样用(语音/电机轮询示例)

Kotlin 复制代码
class MotorAnglePoller {

    private val scheduler = Executors.newScheduledThreadPool(1)
    private var future: ScheduledFuture<*>? = null

    fun start() {
        if (future?.isCancelled == false) return
        future = scheduler.scheduleWithFixedDelay({
            try {
                val motorAngle = readMotorAngle()     // 读取电机角度
                val faceOffset = latestFaceOffset()   // 读共享状态
                adjustMotor(motorAngle, faceOffset)   // 计算并发指令
            } catch (t: Throwable) {
                t.printStackTrace()
            }
        }, 0, 60, TimeUnit.MILLISECONDS) // 约 ~16Hz 轮询
    }

    fun stop() {
        future?.cancel(false)
        future = null
    }

    fun release() {
        stop()
        scheduler.shutdown()
    }
}

什么时候不用它?

  • 需要与生命周期绑紧、UI 线程切换频繁 → 协程 + Lifecycle 更顺手。

  • 需要设备重启后继续、网络/充电条件 → WorkManager

scheduleAtFixedRate 堆积问题

固定频率:1 秒后启动,每 500ms 触发(可能堆积)

ScheduledFuture<?> fixedRate = scheduler.scheduleAtFixedRate(() -> {

try { doWork();

} catch (Exception e) {

e.printStackTrace(); // 一定要捕获异常,否则周期任务会停止 }

}, 1, 500, TimeUnit.MILLISECONDS);

"堆积",本质是:scheduleAtFixedRate 按时钟对齐触发;如果某次 doWork() 比周期(500ms)更慢,调度器会立刻补跑落下的触发(出现"连着跑几次"的感觉)。解决有几条路,按常见程度给你三种"防堆积"方案:

方案 A(最简单):改用固定间隔

scheduleAtFixedRate 换成 scheduleWithFixedDelay,上一轮结束后再等 500ms,天然不堆积。

Kotlin 复制代码
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(() -> {
    try {
        doWork();
    } catch (Exception e) {
        e.printStackTrace();
    }
}, 1, 500, TimeUnit.MILLISECONDS);

适用:允许频率略有波动、但坚决不能补跑/堆积 的场景(大多数业务都适用)。

方案 B(保持固定频率,但忙就跳过

维持 scheduleAtFixedRate 的"时钟对齐"语义,但如果上一轮还在跑,就直接跳过这次,避免补跑。

java 复制代码
import java.util.concurrent.atomic.AtomicBoolean;

AtomicBoolean running = new AtomicBoolean(false);

ScheduledFuture<?> fixedRate = scheduler.scheduleAtFixedRate(() -> {
    if (!running.compareAndSet(false, true)) {
        // 上一轮还没结束 → 跳过当前 tick
        return;
    }
    try {
        doWork();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        running.set(false);
    }
}, 1, 500, TimeUnit.MILLISECONDS);

想"忙完后至少再补一次"(不丢掉最后一次触发),加一个 pending 标志:

java 复制代码
AtomicBoolean running = new AtomicBoolean(false);
AtomicBoolean pending = new AtomicBoolean(false);

scheduler.scheduleAtFixedRate(() -> {
    if (!running.compareAndSet(false, true)) {
        pending.set(true); // 记一笔:有一次触发被跳过
        return;
    }
    try {
        doWork();
        // 如果期间有人标记了 pending,则立刻再跑一次(只补一轮)
        if (pending.getAndSet(false)) {
            doWork();
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        running.set(false);
    }
}, 1, 500, TimeUnit.MILLISECONDS);

适用:要对齐节拍 ,但不能排队的场景(例如传感器处理、UI 心跳)。

方案 C(背压合并:只保留"最新一次")

定时器只负责"投递"工作请求到一个容量=1 的队列;真正的工作在单独线程里执行。队列满就覆盖/丢旧,只保留最新,天然不堆积。

java 复制代码
import java.util.concurrent.*;

ExecutorService worker = Executors.newSingleThreadExecutor();
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);

// 单独的消费者一直处理
worker.execute(() -> {
    for (;;) {
        try {
            Runnable job = queue.take();
            job.run();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            break;
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
});

// 定时触发:尝试把"最新任务"放进队列(满了就丢旧换新)
scheduler.scheduleAtFixedRate(() -> {
    Runnable job = () -> {
        try { doWork(); } catch (Exception e) { e.printStackTrace(); }
    };
    // 清掉旧的,只保留这次("只留最新")
    queue.clear();
    queue.offer(job);
}, 1, 500, TimeUnit.MILLISECONDS);

适用:生产快、消费慢 ,但你只关心最新状态(如状态刷新、数据快照)的场景。

选型建议

  • 业务允许"间隔从完成时刻开始计时" → 方案 A(最简单、最稳)。

  • 必须"按时钟节拍"但不能补跑 → 方案 B(跳过或"只补一次")。

  • 数据是最新优先 ,不需要处理历史 backlog → 方案 C(合并+背压)。

无论哪种方案,记得:

  1. 任务里捕获异常,避免周期任务静默停止;

  2. 周期线程池建议单线程或受控并发,防止并发访问共享资源;

  3. 生命周期结束要 cancel()shutdown(),防泄漏。

下一篇:

ScheduledExecutorService vs Timer/TimerTask核心区别

相关推荐
勇闯逆流河3 小时前
【C++】用红黑树封装map与set
java·开发语言·数据结构·c++
山,离天三尺三3 小时前
深度拷贝详解
开发语言·c++·算法
SpiderPex4 小时前
论MyBatis和JPA权威性
java·mybatis
future_studio4 小时前
聊聊 Unity(小白专享、C# 小程序 之 加密存储)
开发语言·小程序·c#
小糖学代码4 小时前
MySQL:14.mysql connect
android·数据库·mysql·adb
小猪咪piggy4 小时前
【微服务】(1) Spring Cloud 概述
java·spring cloud·微服务
lkbhua莱克瓦244 小时前
Java基础——面向对象进阶复习知识点8
java·笔记·github·学习方法
m0_736927044 小时前
Spring Boot自动配置与“约定大于配置“机制详解
java·开发语言·后端·spring
feiyangqingyun5 小时前
Qt项目作品在苹果macos上编译运行效果/视频监控系统/物联网平台等
开发语言·qt·macos