ScheduledExecutorService 是替代 Timer 的健壮选择,支持线程安全、拒绝策略与异常可见性;应避免 sleep 循环,用 scheduleAtFixedRate 或 scheduleWithFixedDelay 配合 ScheduledFuture 控制生命周期,通过 AtomicBoolean 协同用户输入退出,并统一日志同步以避免竞态。

用 ScheduledExecutorService 启动周期性控制台任务
Java 标准库不推荐用老旧的 Timer,ScheduledExecutorService 是更健壮的选择。它线程安全、支持拒绝策略、能复用线程池,且异常不会静默终止调度。
常见错误是直接 new Thread + sleep 循环——这无法优雅关闭、不处理异常、也不可控暂停/恢复。
- 用
Executors.newScheduledThreadPool(1)创建单线程调度器(多任务需权衡并发数) - 调用
scheduleAtFixedRate()适合固定间隔执行(如每 5 秒打印一次),注意初始延迟设为 0 或正数 - 若任务执行时间可能超过周期(比如耗时 6 秒但设了 5 秒周期),
scheduleAtFixedRate会“追赶”,而scheduleWithFixedDelay则等上一次结束后再延时启动 - 务必保留对
ScheduledFuture的引用,以便后续cancel(true)
如何让控制台任务响应用户输入并安全退出
控制台程序常需按某个键(如 "q")停止调度,但 System.in.read() 是阻塞的,不能和调度器共用一个线程。
典型陷阱是把 Scanner 放在调度任务里轮询输入——这既浪费 CPU,又因输入缓冲行为导致漏读或卡死。
立即学习“Java免费学习笔记(深入)”;
- 开一个独立线程监听
System.in,用System.in.available() > 0配合非阻塞检查(注意:Windows 控制台需回车才触发可用字节) - 更可靠的做法是用
Scanner在主线程阻塞读取,同时用AtomicBoolean running = new AtomicBoolean(true)作为共享状态 - 调度任务开头加
if (!running.get()) return;,主线程收到退出指令后设running.set(false)并调用scheduler.shutdownNow() - 别忘了
shutdownNow()返回未执行的Runnable列表,可用来做清理日志
避免 System.out.println 在调度中引发乱序或丢失
多个线程(调度线程 + 输入监听线程 + 主线程)同时写控制台,可能输出错行、字符截断,甚至部分日志完全不显示。
这不是 bug,而是 PrintStream 虽线程安全,但每次 println 是“获取锁 → 写内容 → 换行”三步,中间可能被其他线程插入。
- 所有日志统一走
System.out.printf("[%s] %s%n", Thread.currentThread().getName(), msg),便于排查来源 - 若需强一致性(比如关键状态快照),用
synchronized (System.out) { System.out.println(...); } - 生产级建议替换为
java.util.logging.Logger或slf4j,它们默认同步且支持异步 Appender - 避免在调度任务里拼接长字符串再打印——先格式化好再输出,减少锁持有时间
调试时怎么看任务是否真在运行、有没有堆积
控制台看不到线程池内部状态,仅靠打印 “task executed” 容易误判:可能任务已提交但线程池满、或异常抛出后被吞掉。
最常忽略的是未捕获异常——ScheduledExecutorService 默认不打印堆栈,任务一崩就静默停摆。
- 给调度器传入自定义
ThreadFactory,为线程命名(如"scheduler-pool-%d"),方便 jstack 查看 - 包装任务:用
try-catch(Throwable t)包住整个Runnable体,并打印t.printStackTrace()到System.err - 通过
scheduler.awaitTermination(1, TimeUnit.SECONDS)测试 shutdown 是否真正结束(返回 false 表示还有活跃任务) - 临时加一行
System.out.println("Pool size: " + ((ScheduledThreadPoolExecutor)scheduler).getActiveCount());看当前忙线程数










