ScheduledExecutorService
引言
ScheduledExecutorService 是啥?
Java 的定时任务线程池。用它可以:
-
延时执行一次任务:
schedule(...)
-
按固定频率循环执行:
scheduleAtFixedRate(...)
-
按固定间隔循环执行:
scheduleWithFixedDelay(...)
三个核心方法差异
schedule(task, delay, unit)
:延时一次。
scheduleAtFixedRate(task, initialDelay, period, unit)
:按“时钟频率”跑;不等上一个任务结束的时间来对齐下一个开始时间(可能堆积)。
scheduleWithFixedDelay(task, initialDelay, delay, unit)
:上一个结束后再等delay
才开始下一个(不堆积,更稳)。
使用
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 常用)
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
常见坑 & 最佳实践
- 一定捕获异常:周期任务里抛出未捕获异常会让该周期任务直接停止。
- 不要长时间阻塞定时线程:如果任务很慢,用线程池执行实际工作,定时器只负责“触发”。
- 合理线程数:
newScheduledThreadPool(1)
足够多数场景;需要并行周期任务时再加大线程数。
- 取消要清理:保存
ScheduledFuture<?>
,在onStop()/onDestroy()
里cancel()
,再shutdown()
。
- Android UI:它跑在后台线程,不能直接操作 UI;需要切回主线程(
Handler
/runOnUiThread
/LiveData
/Flow
)。
- Android 替代:简单周期任务可用 Kotlin 协程:
-
scope.launch {delay(1000)while (isActive) {doWork()delay(500) // 等价于 fixedDelay} }
需要约束(充电/Wi-Fi/重启恢复)请选择 WorkManager。
项目里可以这样用(语音/电机轮询示例)
class MotorAnglePoller {private val scheduler = Executors.newScheduledThreadPool(1)private var future: ScheduledFuture<*>? = nullfun start() {if (future?.isCancelled == false) returnfuture = 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,天然不堆积。
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(() -> {try {doWork();} catch (Exception e) {e.printStackTrace();}
}, 1, 500, TimeUnit.MILLISECONDS);
适用:允许频率略有波动、但坚决不能补跑/堆积 的场景(大多数业务都适用)。
方案 B(保持固定频率,但忙就跳过)
维持 scheduleAtFixedRate
的“时钟对齐”语义,但如果上一轮还在跑,就直接跳过这次,避免补跑。
import java.util.concurrent.atomic.AtomicBoolean;AtomicBoolean running = new AtomicBoolean(false);ScheduledFuture<?> fixedRate = scheduler.scheduleAtFixedRate(() -> {if (!running.compareAndSet(false, true)) {// 上一轮还没结束 → 跳过当前 tickreturn;}try {doWork();} catch (Exception e) {e.printStackTrace();} finally {running.set(false);}
}, 1, 500, TimeUnit.MILLISECONDS);
想“忙完后至少再补一次”(不丢掉最后一次触发),加一个 pending
标志:
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的队列;真正的工作在单独线程里执行。队列满就覆盖/丢旧,只保留最新,天然不堆积。
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(合并+背压)。
无论哪种方案,记得:
任务里捕获异常,避免周期任务静默停止;
周期线程池建议单线程或受控并发,防止并发访问共享资源;
生命周期结束要
cancel()
并shutdown()
,防泄漏。
下一篇:
ScheduledExecutorService vs Timer/TimerTask核心区别