System.out.println() 多线程乱序因 PrintStream 非线程安全,缓冲、写入、换行等步骤被交叉打断;Log4j2/SLF4J 正确配置可避免;加锁性能差,推荐异步日志或文件追加;验证应检查单行完整性而非显示顺序。

为什么 System.out.println() 在多线程下会乱序
因为 System.out 是一个共享的 PrintStream,其内部缓冲区和底层 write() 调用不是原子的。多个线程同时调用 println() 时,可能在「写入缓冲区」「刷出到控制台」「换行符插入」等环节被交叉打断。比如线程 A 写了 "[T1] start" 的前半截,线程 B 插入了一整行,A 再补上 "end",最终输出就变成 [T1] sta[T2] done\nrt 这类不可读内容。
这不是 JVM bug,而是未加同步的典型竞态表现。即使每行 println() 看似“一条语句”,它实际对应多个字节写入操作,而标准输出流不保证这些操作的线程安全。
用 Log4j2 或 SLF4J + Logback 能避免乱序吗
能,但前提是正确配置。默认情况下,Log4j2 的 ConsoleAppender 启用 follow="true" 且使用 PatternLayout 时,日志事件是先格式化成完整字符串、再原子写入的——这依赖于底层 Writer 的线程安全性(如 OutputStreamWriter 在 JDK 中对 write() 加了同步)。
不过要注意几个关键点:
立即学习“Java免费学习笔记(深入)”;
- 避免在
PatternLayout中使用非线程安全的自定义Converter - 不要把
Logger实例缓存为静态变量后,在不同线程里反复调用logger.info("msg", obj)时传入可变对象(如StringBuilder),否则格式化阶段仍可能出问题 - 若使用异步日志(
AsyncLogger),需确认RingBuffer大小和等待策略,极端高并发下仍可能因队列满而退化为同步写
自己加锁实现线程安全打印的代价有多大
直接用 synchronized(System.out) 包裹 println() 是最简单的方式,但会严重拖慢性能:所有线程串行写控制台,吞吐量归零,还可能引发锁竞争和上下文切换开销。更糟的是,如果某线程在锁内执行耗时操作(比如拼接大字符串或调用外部方法),其他线程会长时间阻塞。
可行的轻量替代方案:
- 每个线程用
ThreadLocal缓存日志内容,最后统一 flush —— 适合批处理场景 - 用
java.util.concurrent.BlockingQueue做日志中转,单个消费者线程顺序写入 —— 引入延迟但解耦清晰 - 改用
Files.write()写文件而非控制台,配合StandardOpenOption.APPEND(OS 层保证追加原子性)
验证是否真的解决乱序:看日志时间戳还是行首标识
别只盯着控制台输出顺序。真正可靠的验证方式是检查每条日志是否包含完整、无截断的信息。例如,用如下格式:
2024-06-15T14:22:33.123 [T-7] START processing item=1001 2024-06-15T14:22:33.124 [T-7] END processing item=1001, cost=12ms
如果出现 2024-06-15T14:22:33.123 [T-7] START proce2024-06-15T14:22:33.124 [T-8] DONE 这种时间戳和线程标识混在一起的情况,说明仍有乱序;而只要每行开头有完整时间戳+线程名+动作,哪怕整体时间顺序不严格,也表明单行内容是完整的——这才是并发日志的核心目标。
最容易被忽略的一点:IDE 控制台(如 IntelliJ 的 Run Console)本身就有渲染缓冲和行合并逻辑,有时显示乱序未必是程序问题,建议重定向输出到文件后再用 less 或 cat 查看原始字节流。











