引言
ScheduledExecutorService 是啥?
Java 的定时任务线程池。用它可以:
-
延时 执行一次任务:
schedule(...)
-
按固定频率 循环执行:
scheduleAtFixedRate(...)
-
按固定间隔 循环执行:
scheduleWithFixedDelay(...)
三个核心方法差异
schedule(task, delay, unit)
:延时一次。scheduleAtFixedRate(task, initialDelay, period, unit)
:按"时钟频率"跑;不等上一个任务结束的时间来对齐下一个开始时间(可能堆积)。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
常见坑 & 最佳实践
- 一定捕获异常 :周期任务里抛出未捕获异常会让该周期任务直接停止。
- 不要长时间阻塞 定时线程:如果任务很慢,用线程池执行实际工作,定时器只负责"触发"。
- 合理线程数 :
newScheduledThreadPool(1)
足够多数场景;需要并行周期任务时再加大线程数。 - 取消要清理 :保存
ScheduledFuture<?>
,在onStop()/onDestroy()
里cancel()
,再shutdown()
。 - Android UI :它跑在后台线程,不能直接操作 UI ;需要切回主线程(
Handler
/runOnUiThread
/LiveData
/Flow
)。 - 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(合并+背压)。
无论哪种方案,记得:
任务里捕获异常,避免周期任务静默停止;
周期线程池建议单线程或受控并发,防止并发访问共享资源;
生命周期结束要
cancel()
并shutdown()
,防泄漏。
下一篇: