stringbuilder线程不安全的根本原因是其char[] value和int count两个关键字段的并发读写缺乏原子保护,append操作中扩容、写入、count更新三步非原子且无同步机制,导致数据覆盖、越界或丢失。

StringBuilder 线程不安全,根本原因不在“没加锁”这个表象,而在于其核心状态变量的并发读写缺乏原子保护——特别是 char[] value 和 int count 这两个关键字段的协同操作被多个线程交叉破坏。
底层共享状态未受保护
StringBuilder(实际由父类 AbstractStringBuilder 实现)用一个可变 char 数组和一个计数器 count 记录当前长度。append 操作本质是两步:
- 检查数组容量是否足够,不够则扩容(可能触发 Arrays.copyOf)
- 把新字符写入 value[count],然后 count++
这两步不是原子的。多个线程同时执行时,可能出现:线程 A 判断 count=999、value 长度=1000,认为无需扩容;线程 B 同样判断后也准备写入;结果两者都往 value[999] 写,或一个写完 count 变成 1000,另一个仍用旧 count 覆盖——造成数据丢失或 ArrayIndexOutOfBoundsException。
方法无 synchronized 修饰
对比 StringBuffer,它的所有 public 修改方法(append、insert、delete 等)都声明为 synchronized,锁住 this 实例。这意味着:
- 同一时刻只有一个线程能进入 append 方法体
- value 和 count 的读、写、扩容全部被包裹在临界区内
- JVM 保证 synchronized 块内对共享变量的修改对其他线程可见(happens-before)
StringBuilder 完全没有这类同步机制,方法调用等价于裸奔访问共享内存。
Java开发手册规约集合,基于阿里巴巴Java开发手册(嵩山版)。 涵盖7大维度:编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程结构、设计规约。 当用户需要:(1) 编写或审查Java代码 (2) 检查命名/代码规范 (3) 处理异常和日志 (4) 编写单元测试 (5) 安全编码 (6) 数据库设...
扩容逻辑加剧竞争风险
当 value 数组需要扩容时,AbstractStringBuilder 会调用 ensureCapacityInternal → newCapacity → Arrays.copyOf。这个过程本身耗时,且涉及新数组分配、旧内容复制。多线程下极易出现:
- 线程 A 刚完成扩容,value 指向新数组,count 更新完毕
- 线程 B 还拿着旧数组引用,继续往旧 value 写,直接越界
- 或两个线程各自扩容,但只有一份结果被保留,另一份丢失
这种非幂等、非原子的扩容行为,在无锁环境下天然不可靠。
它不是“偶尔出错”,而是“必然不可预测”
面试中常被问“会不会抛异常?”答案是:可能不抛异常,但结果错误;也可能抛 ArrayIndexOutOfBoundsException;还可能静默丢数据。因为:
- JVM 不保证非同步访问的内存可见性,count 可能被缓存在 CPU 寄存器中,线程看不到别人改过的值
- 没有 happens-before 关系,编译器或处理器可能重排序指令,让写 count 提前于写 value
- 问题复现依赖线程调度、CPU 核心数、JVM 版本,调试极其困难
所以这不是“概率低的小问题”,而是并发模型层面的根本缺陷。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










