高并发日志卡顿的核心是锁粒度与i/o阻塞,应采用异步日志框架(如log4j2/logback+disruptor)、占位符打印、stringbuilder+threadlocal拼接、禁用immediateflush及合理滚动策略。

高并发下日志卡顿,核心问题不是“要不要加锁”,而是“锁在哪里、锁了什么、锁了多久”。直接用 StringBuffer 或对整个日志方法加 synchronized,等于把所有线程堵在同一个窄门口——吞吐量必然断崖下跌。
用异步日志框架替代同步写入
这是最直接有效的解法。Log4j2 的 AsyncAppender 或 Logback 的 AsyncAppender 会把日志事件丢进无锁环形队列(如 Disruptor),由独立后台线程消费并落盘。业务线程不参与 I/O,延迟从毫秒级降到微秒级。
- Log4j2 需显式引入
disruptor依赖,并配置<asynclogger></asynclogger>或<asyncroot></asyncroot> - 避免混用同步与异步 Logger:同一类中不要既有
Logger.getLogger()又有AsyncLogger.getContext().getLogger() - 异步模式下,
immediateFlush=false是默认且推荐的,禁用它才能发挥缓冲优势
避免日志语句本身触发锁或计算
很多卡顿其实发生在“打日志之前”——比如字符串拼接、对象 toString()、条件判断等,这些操作在日志未启用时也照常执行,白白消耗 CPU 和锁资源。
- 禁用
logger.debug("User: " + user.getName())这类拼接,改用占位符:logger.debug("User: {}", user) - 对复杂对象,优先用
logger.isInfoEnabled()做前置判断,再执行耗时构造(如 JSON 序列化) - 避免在日志参数里调用数据库查询、远程接口或锁住共享资源的方法
按场景选择线程安全策略,而非硬套 StringBuffer
StringBuffer 是全方法 synchronized,每次 append() 都抢同一把锁,在高并发日志拼接中毫无优势。更合理的做法是:
- 每个线程用
StringBuilder拼接本地日志内容,拼完再交给日志框架——零锁竞争 - 若需复用缓冲对象,用
ThreadLocal<stringbuilder></stringbuilder>,既避免频繁创建,又不共享状态 - 完全没必要自己基于
StringBuffer写简易日志器;真要轻量,可用 SLF4J + NOP 绑定做空实现,或直接走异步框架的最小配置
合理配置滚动与缓冲,减少 I/O 频次
频繁小块写磁盘比锁竞争更伤性能。一次写 1KB 比 100 次写 10 字节快得多,尤其在机械盘或高负载 SSD 上。
- 启用文件缓冲:Log4j2 中设置
<bufferedwriter buffersize="8192"></bufferedwriter> - 滚动策略选组合型,例如“每天滚动 + 单文件不超过 100MB”,避免单文件过大或数量爆炸
- 禁用
immediateFlush=true(除非强一致性审计场景),让 OS 层决定刷盘时机
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











