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或加同步)。 - 灵活应对:预判面试官提问,准备简洁有力的回答,展示对底层实现的理解。