单线程拼接优先用 stringbuilder,因其无锁、执行路径短;jmh 测试显示其性能约为 stringbuffer 的 2 倍以上;多线程仅在真正共享同一实例时才考虑 stringbuffer,否则推荐 threadlocal 或每线程独立实例。

直接看场景,不看理论。
单线程拼接,选 StringBuilder
它没锁,复用同一套底层逻辑(AbstractStringBuilder),执行路径短。JMH 实测 10 万次 append:
- StringBuilder 约 12ms
- StringBuffer 约 28ms
差距不是“一点点”,是 2 倍以上。
常见于:方法内临时拼接、Stream.collect()、SQL 构造、JSON 组装、日志本地格式化等。
多线程共享同一个实例,才考虑 StringBuffer
注意关键词是“共享同一个实例”。比如:
- Servlet 的全局日志缓冲区(多个请求线程往里写)
- 定时任务中多个线程共用一个 XML 构建器
- 线程池里多个 Worker 共同向同一 SQL 拼接器追加条件
但这类设计本身要谨慎——更推荐每个线程用自己 StringBuilder,或配合 ThreadLocal:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
private static final ThreadLocal<stringbuilder> tl =
ThreadLocal.withInitial(() -> new StringBuilder(1024));</stringbuilder>
别被“可能并发”带偏
如果变量只在方法栈内创建、用完即弃(局部变量),哪怕方法被多线程调用,也完全不用 synchronized。
StringBuffer 在这种场景下纯属性能浪费,还掩盖真实线程模型问题。
压测时别手写 System.currentTimeMillis()
微秒级差异容易被 GC、JIT 预热、系统调度干扰。靠谱做法是用 JMH:
- 加 @Warmup(5) 让 JIT 编译稳定
- @Fork(3) 隔离进程环境
- @Measurement(5) 多轮取平均
- 用 Blackhole.consume() 防止 JVM 优化掉计算
- 测试不同规模(100 / 1000 / 10000 次),看趋势而非单点值
扩容影响比锁还隐蔽
两者默认容量都是 16,扩容公式一致:newCap = oldCap × 2 + 2。
拼接前预估长度并指定初始容量,比纠结用谁更有效:
- 拼接 500 字符的 CSV?
new StringBuilder(600) - 构造含 20 个字段的 JSON?
new StringBuilder(1024)
避免反复 copy 数组,这开销有时比锁还大。
不复杂但容易忽略。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










