StringBuilder 和 StringBuffer 的区别:源码分析与面试准备
在 Java 中,StringBuilder
和 StringBuffer
都是用来处理可变字符串的类,它们的主要功能是通过动态拼接字符串来避免 String
的不可变性带来的性能开销。尽管它们功能相似,但在实现细节和使用场景上有显著差异。以下从源码角度分析两者的区别,并探讨"是否所有方法都加了锁"这个问题。
一、核心区别
-
线程安全性
StringBuffer
:线程安全。它的绝大部分方法都通过synchronized
关键字加了锁,以确保在多线程环境下操作的安全性。StringBuilder
:非线程安全。它没有加锁机制,因此在单线程环境下性能更高,但在多线程场景中需要外部同步。
-
性能
StringBuffer
的同步机制带来了额外的性能开销,尤其在高并发场景下。StringBuilder
没有同步开销,适合单线程或无需线程安全的场景。
-
继承关系
- 两者都继承自
AbstractStringBuilder
类,共享相同的底层实现(如字符数组存储、容量扩展逻辑)。区别主要在于StringBuffer
覆写了方法并加锁,而StringBuilder
直接复用父类逻辑。
- 两者都继承自
二、源码分析
让我们从 JDK 源码(以 JDK 11 为例)中看看具体实现。
1. 构造方法
-
StringBuffer
:javapublic StringBuffer() { super(16); // 默认容量为 16 }
-
StringBuilder
:javapublic StringBuilder() { super(16); // 默认容量为 16 }
两者构造方法都调用了 AbstractStringBuilder
的构造方法,初始化一个默认容量为 16 的字符数组。区别不在构造,而在后续操作。
2. append 方法(最常用)
-
StringBuffer
:java@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
synchronized
确保线程安全。- 调用父类的
append
,实际操作字符数组。
-
StringBuilder
:java@Override public StringBuilder append(String str) { super.append(str); return this; }
- 没有
synchronized
,直接调用父类实现。
- 没有
3. delete 方法
-
StringBuffer
:java@Override public synchronized StringBuffer delete(int start, int end) { toStringCache = null; super.delete(start, end); return this; }
-
StringBuilder
:javapublic StringBuilder delete(int start, int end) { super.delete(start, end); return this; }
4. 是否所有方法都加锁?
- StringBuffer :并非所有方法都加锁。例如:
-
toString()
:java@Override public synchronized String toString() { if (toStringCache == null) { toStringCache = Arrays.copyOfRange(value, 0, count); } return new String(toStringCache, 0, toStringCache.length); }
toString()
加了锁,但length()
和capacity()
等方法未加锁:javapublic int length() { return count; }
原因是这些方法只读取状态,不修改底层数据,不涉及线程竞争。
-
- StringBuilder:无任何方法加锁,全部依赖父类实现。
三、面试官可能提出的问题及回答
-
"为什么 StringBuffer 是线程安全的?"
- 回答:
StringBuffer
通过在关键方法(如append
、delete
)上加synchronized
锁,确保多线程环境下对底层字符数组的修改是原子的,避免数据不一致。 - 源码支持:见上述
append
方法的synchronized
关键字。
- 回答:
-
"所有方法都加锁吗?有什么例外?"
- 回答:不是所有方法都加锁。
StringBuffer
的修改方法(如append
、insert
)加了锁,但只读方法(如length
、capacity
)未加锁,因为它们不改变状态,无线程安全问题。 - 思路:区分读写操作,结合源码举例。
- 回答:不是所有方法都加锁。
-
"StringBuilder 和 StringBuffer 的性能差距有多大?"
- 回答:在单线程环境下,
StringBuilder
因为无同步开销,性能更高。差距取决于操作频率和规模,在高并发场景下StringBuffer
的锁竞争可能导致显著延迟。 - 思路:从 JVM 锁机制(如锁膨胀)延伸,强调场景选择。
- 回答:在单线程环境下,
-
"如果在多线程中用 StringBuilder 会怎样?"
- 回答:可能导致数据混乱或异常(如数组越界),因为多个线程同时修改共享的字符数组无同步保护。可以用
synchronized
块或StringBuffer
替代。 - 思路:举例说明竞争条件,结合实际场景。
- 回答:可能导致数据混乱或异常(如数组越界),因为多个线程同时修改共享的字符数组无同步保护。可以用
四、总结回答思路
- 结构化表达:先讲核心区别(线程安全、性能),再深入源码(方法实现、锁使用),最后结合问题延伸。
- 源码支撑 :引用具体方法(如
append
、toString
),突出加锁与否的差异。 - 场景导向 :强调选择依据(单线程用
StringBuilder
,多线程用StringBuffer
或加同步)。 - 灵活应对:预判面试官提问,准备简洁有力的回答,展示对底层实现的理解。