关于前端:为什么StringBuilder是线程不安全的

36次阅读

共计 3812 个字符,预计需要花费 10 分钟才能阅读完成。

前咱们比照了 String、StringBuilder 和 StringBuffer 的区别,其中一项便提到 StringBuilder 是非线程平安的,那么是什么起因导致了 StringBuilder 的线程不平安呢?

起因剖析
如果你看了 StringBuilder 或 StringBuffer 的源代码会说,因为 StringBuilder 在 append 操作时并未应用线程同步,而 StringBuffer 简直大部分办法都应用了 synchronized 关键字进行办法级别的同步解决。

下面这种说法必定是正确的,对照一下 StringBuilder 和 StringBuffer 的局部源代码也可能看进去。

StringBuilder 的 append 办法源代码:

@Overridepublic StringBuilder append(String str) {super.append(str); return this;
}
StringBuffer 的 append 办法源代码:

@Overridepublic synchronized StringBuffer append(String str) {

toStringCache = null;    super.append(str);    return this;

}
对于下面的论断必定是没什么问题的,但并没有解释是什么起因导致了 StringBuilder 的线程不平安?为什么要应用 synchronized 来保障线程平安?如果不 <typo id=”typo-679″ data-origin=” 是用 ” ignoretag=”true”> 是用 </typo> 会呈现什么异常情况?

上面咱们来逐个解说。

异样示例
咱们先来跑一段代码示例,看看呈现的后果是否与咱们的预期统一。

@Testpublic 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 存在的意义,因为数组中的一部分内容为默认值。如 www.sangpi.com 当调用 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。这种状况一旦产生便不会呈现预期的后果。

抛异样的起因
回头看异样的堆栈信息,<typo id=”typo-2537″ data-origin=” 回 ” ignoretag=”true”> 回 </typo> 发现有这么一行内容:

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 的线程不平安的起因?咱们在学习和实际的过程中,不仅要晓得一些论断,还要晓得这些论断的底层原理,更重要的是学会剖析底层原理的办法。

正文完
 0