▷ 为什么StringBuilder是线程不安全的?

⌹ 365提款成功但是不到账 ⏱️ 2026-07-13 04:53:47 👤 admin 👁️‍🗨️ 4617 ❤️ 225
为什么StringBuilder是线程不安全的?

文章来源于公众号:程序新视界 作者:丑胖侠二师兄

在前面的面试题讲解中我们对比了 String、StringBuilder和StringBuffer的区别 ,其中一项便提到 StringBuilder 是非线程安全的,那么是什么原因导致了 StringBuilder 的线程不安全呢?

原因分析

如果你看了 StringBuilder 或 StringBuffer 的源代码会说,因为StringBuilder在append操作时并未使用线程同步,而StringBuffer几乎大部分方法都使用了synchronized关键字进行方法级别的同步处理。

上面这种说法肯定是正确的,对照一下StringBuilder和StringBuffer的部分源代码也能够看出来。

StringBuilder的append方法源代码:

@Override

public StringBuilder append(String str) {

super.append(str);

return this;

}

StringBuffer的append方法源代码:

@Override

public synchronized StringBuffer append(String str) {

toStringCache = null;

super.append(str);

return this;

}

对于上面的结论肯定是没什么问题的,但并没有解释是什么原因导致了StringBuilder的线程不安全?为什么要使用synchronized来保证线程安全?如果不是用会出现什么异常情况?

下面我们来逐一讲解。

异常示例

我们先来跑一段代码示例,看看出现的结果是否与我们的预期一致。

@Test

public void test() throws InterruptedException {

StringBuilder sb = new StringBuilder();

for (int i = 0; i < 10; i++) {

new Thread(() -> {

for (int j = 0; j < 1000; j++) {

sb.append("a");

}

}).start();

}

// 睡眠确保所有线程都执行完

Thread.sleep(1000);

System.out.println(sb.length());

}

上述业务逻辑比较简单,就是构建一个StringBuilder,然后创建10个线程,每个线程中拼接字符串“a”1000次,理论上当线程执行完成之后,打印的结果应该是10000才对。

但多次执行上面的代码打印的结果是10000的概率反而非常小,大多数情况都要少于10000。同时,还有一定的概率出现下面的异常信息“

Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException

at java.lang.System.arraycopy(Native Method)

at java.lang.String.getChars(String.java:826)

at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449)

at java.lang.StringBuilder.append(StringBuilder.java:136)

at com.secbro2.strings.StringBuilderTest.lambda$test$0(StringBuilderTest.java:18)

at java.lang.Thread.run(Thread.java:748)

9007

线程不安全的原因

StringBuilder中针对字符串的处理主要依赖两个成员变量char数组value和count。StringBuilder通过对value的不断扩容和count对应的增加来完成字符串的append操作。

// 存储的字符串(通常情况一部分为字符串内容,一部分为默认值)

char[] value;

// 数组已经使用数量

int count;

上面的这两个属性均位于它的抽象父类AbstractStringBuilder中。

如果查看构造方法我们会发现,在创建StringBuilder时会设置数组value的初始化长度。

public StringBuilder(String str) {

super(str.length() + 16);

append(str);

}

默认是传入字符串长度加16。这就是count存在的意义,因为数组中的一部分内容为默认值。

当调用append方法时会对count进行增加,增加值便是append的字符串的长度,具体实现也在抽象父类中。

public AbstractStringBuilder append(String str) {

if (str == null)

return appendNull();

int len = str.length();

ensureCapacityInternal(count + len);

str.getChars(0, len, value, count);

count += len;

return this;

}

我们所说的线程不安全的发生点便是在append方法中count的“+=”操作。我们知道该操作是线程不安全的,那么便会发生两个线程同时读取到count值为5,执行加1操作之后,都变成6,而不是预期的7。这种情况一旦发生便不会出现预期的结果。

抛异常的原因

回头看异常的堆栈信息,回发现有这么一行内容:

at java.lang.String.getChars(String.java:826)

对应的代码就是上面AbstractStringBuilder中append方法中的代码。对应方法的源代码如下:

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {

if (srcBegin < 0) {

throw new StringIndexOutOfBoundsException(srcBegin);

}

if (srcEnd > value.length) {

throw new StringIndexOutOfBoundsException(srcEnd);

}

if (srcBegin > srcEnd) {

throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);

}

System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);

}

其实异常是最后一行arraycopy时JVM底层发生的。arraycopy的核心操作就是将传入的String对象copy到value当中。

而异常发生的原因是明明value的下标只到6,程序却要访问和操作下标为7的位置,当然就跑异常了。

那么,为什么会超出这么一个位置呢?这与我们上面讲到到的count被少加有关。在执行str.getChars方法之前还需要根据count校验一下当前的value是否使用完毕,如果使用完了,那么就进行扩容。append中对应的方法如下:

ensureCapacityInternal(count + len);

ensureCapacityInternal的具体实现:

private void ensureCapacityInternal(int minimumCapacity) {

// overflow-conscious code

if (minimumCapacity - value.length > 0) {

value = Arrays.copyOf(value,

newCapacity(minimumCapacity));

}

}

count本应该为7,value长度为6,本应该触发扩容。但因为并发导致count为6,假设len为1,则传递的minimumCapacity为7,并不会进行扩容操作。这就导致后面执行str.getChars方法进行复制操作时访问了不存在的位置,因此抛出异常。

这里我们顺便看一下扩容方法中的newCapacity方法:

private int newCapacity(int minCapacity) {

// overflow-conscious code

int newCapacity = (value.length << 1) + 2;

if (newCapacity - minCapacity < 0) {

newCapacity = minCapacity;

}

return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)

? hugeCapacity(minCapacity)

: newCapacity;

}

除了校验部分,最核心的就是将新数组的长度扩充为原来的两倍再加2。把计算所得的新长度作为Arrays.copyOf的参数进行扩容。

小结

经过上面的分析,是不是真正了解了StringBuilder的线程不安全的原因?我们在学习和实践的过程中,不仅要知道一些结论,还要知道这些结论的底层原理,更重要的是学会分析底层原理的方法。

以上就是W3Cschool编程狮关于为什么StringBuilder是线程不安全的?的相关介绍了,希望对大家有所帮助。

◈ 相关文章

HTML 页面结构​
⌹ 365bet现场滚球

▷ HTML 页面结构​

⏱️ 12-04 👁️‍🗨️ 8304
德州到北京距离 德州到北京多远多少公里?
⌹ 365bet现场滚球

▷ 德州到北京距离 德州到北京多远多少公里?

⏱️ 01-25 👁️‍🗨️ 2571
大日本帝国
⌹ 365bet线上

▷ 大日本帝国

⏱️ 08-09 👁️‍🗨️ 645