优先选StringBuilder,仅当多线程共享同一实例时才用StringBuffer;两者API相同但StringBuffer所有方法加synchronized锁,单线程下性能差3倍以上。

StringBuffer 和 StringBuilder 哪个该用?先看线程是否共享
结论很直接:**如果多个线程会同时读写同一个字符串对象,必须用 StringBuffer;否则一律选 StringBuilder。** 别猜,别“以防万一”,就看变量是不是跨线程传递或全局共享。
常见错误现象:StringBuilder 被当成静态字段或注入到 Spring Bean 中被多线程复用,结果拼出的 JSON 或 SQL 随机缺字段、乱序、甚至抛 ArrayIndexOutOfBoundsException——这不是 Bug,是并发冲突。
- 单线程场景(如 Controller 方法内拼接返回值、循环构建日志内容)→ 用
StringBuilder - 线程局部变量(
ThreadLocal<StringBuilder>)→ 仍可用StringBuilder,安全且高效 - 全局缓存中拼接配置字符串(如
public static final StringBuffer CONFIG_BUILDER)→ 必须用StringBuffer
为什么 StringBuffer 更慢?锁不是白加的
StringBuffer 每个公开方法(append()、insert()、toString())都带 synchronized,意味着同一时刻只能一个线程执行——哪怕你只是在做只读的 length()(它也同步),哪怕数组容量绰绰有余。
性能差异不是“稍微慢点”:在单线程下做 10 万次 append("a"),StringBuilder 通常耗时 8–12ms,StringBuffer 往往要 35–45ms。差距来自锁获取/释放、内存屏障、JVM 对同步块的保守优化。
立即学习“Java免费学习笔记(深入)”;
- 不要因为“以后可能多线程”就提前用
StringBuffer—— 这属于过早优化,且牺牲了确定的性能 - 如果真要升级到多线程共享,光换类不够,还得检查整个调用链是否可重入、是否隐式共享了内部
char[] -
toString()方法在两者中都会触发新String对象创建,但StringBuffer多一次锁进出开销
API 完全一样,但底层继承关系暴露了真相
它们都继承自 AbstractStringBuilder,共用同一套扩容逻辑、字符数组操作和缓冲区管理。区别仅在于:一个在方法签名上写了 synchronized,另一个没写。
这意味着你完全可以把 StringBuilder 的代码复制粘贴过去改个类名就能编译通过——但运行结果可能错得离谱。
- 构造时默认初始容量都是
16,频繁拼接建议显式指定(如new StringBuilder(256)),避免多次扩容拷贝 - 两者都不支持
final修饰的底层char[],所以都能修改内容;而String的value是final char[],这是不可变性的物理保障 - 别试图给
StringBuilder加synchronized包一层来“手动线程安全”——这既不能保证原子性(比如sb.append("a").append("b")是两步),又破坏了设计契约
容易被忽略的坑:toString() 后的引用陷阱
很多人以为 sb.toString() 只是“转一下类型”,其实它会创建新 String 对象,并拷贝当前 count 长度的字符。更关键的是:这个 String 和 StringBuilder 内部的 char[] 彻底无关了。
但如果你在 toString() 后继续用同一个 StringBuilder 实例拼接,旧的 String 不会变——这点没错;可一旦你误以为“toString() 返回的就是内部数组的视图”,就可能写出依赖内存地址的错误逻辑。
- 不要缓存
toString()结果并期望它随StringBuilder变化而更新(它不会) - 不要把
StringBuilder当成“可变 String 引用”来传参,尤其在异步回调里——它的状态随时可能被其他代码改掉 - 如果需要“构建后不可再改”,拼完立刻调用
toString()并丢弃 builder,别留着复用
真正麻烦的从来不是选哪个类,而是搞不清变量的作用域和生命周期。线程安全不是加个 synchronized 就万事大吉,而是对数据归属的清晰界定。











