多线程共享场景必须用StringBuffer,因其方法均synchronized保证单次调用原子性;单线程优先选StringBuilder以提升10%–20%性能;构造时应预估容量避免频繁扩容;toString()返回新String,修改buffer不影响已有字符串副本。

多线程环境下必须用 StringBuffer
当字符串操作发生在多个线程共享的上下文中(比如 Servlet 的成员变量、静态工具类方法),StringBuffer 是唯一安全的选择。它的所有公开方法(如 append()、insert()、reverse())都加了 synchronized,能保证操作原子性。
但要注意:同步只覆盖单个方法调用,不保证连续调用的原子性。例如:
buffer.append("a").append("b"); // 两次 synchronized 方法调用,中间可能被其他线程插入常见误用场景:
- 把
StringBuffer当作“线程安全的 StringBuilder”直接塞进高并发循环里,却忽略了锁竞争带来的性能断崖 - 在局部方法内(无共享对象)仍用
StringBuffer,纯属浪费同步开销
单线程性能敏感场景一律选 StringBuilder
StringBuilder 是 StringBuffer 的非同步镜像,API 完全一致,但去掉所有 synchronized。在大多数本地字符串拼接、JSON 构建、模板渲染等场景中,它比 StringBuffer 快 10%–20%,JVM 还可能对其做额外优化(如逃逸分析后栈上分配)。
立即学习“Java免费学习笔记(深入)”;
典型适用情况:
- 方法内临时拼接:
new StringBuilder().append(name).append("@").append(domain).toString() - 循环内累积日志:
for (String s : list) { builder.append(s).append("\n"); } - Spring MVC 中
@ResponseBody返回前的 JSON 组装
注意:不要复用 StringBuilder 实例跨方法传递,除非你明确控制其生命周期和线程归属——否则容易引发状态污染或内存泄漏。
构造时指定容量能避免多次数组扩容
两者底层都用 char[] 数组存储,初始容量默认为 16。如果预估最终长度远超 16(比如拼接 500 个字符串),不设初始容量会导致频繁扩容(每次约 1.5 倍增长 + 数组拷贝),显著拖慢性能。
推荐做法:
- 知道大致长度时,直接传入:
new StringBuilder(1024)或new StringBuffer(2048) - 拼接已知集合,用
list.size() * avgLength估算(比如 100 个平均 20 字符的字符串 → 初始容量设为 2500) - 不确定长度但量级大,宁可略高估(如设 8192),也别依赖默认值反复扩容
扩容代价在 GC 日志中常表现为大量 char[] 短生命周期对象,可通过 -XX:+PrintGCDetails 观察。
别混淆 toString() 和 String.valueOf()
StringBuffer.toString() 和 StringBuilder.toString() 都返回新 String 对象,内容是当前缓冲区快照;而 String.valueOf(buffer) 会调用其 toString(),本质相同。但有人误以为 String.valueOf() 更“轻量”,其实没有区别。
真正要警惕的是这个反模式:
String s = buffer.toString(); // ✅ 正确:获取副本 buffer.setLength(0); // ❌ 错误:清空后 s 不受影响,但若后续继续用 buffer,得确保没人引用旧 toString 结果
更隐蔽的问题:某些框架(如旧版 Log4j)内部缓存了 toString() 结果,如果之后修改了 buffer 内容,日志可能输出意外值——这和 String 不可变的设计初衷相悖,务必隔离缓冲区生命周期。










