流光年华|为什么?为什么StringBuilder是线程不安全的?


  • 周一福利到!献上“独家全新”MySQL进阶套餐 , 简直就是血赚
  • 死磕「并发编程」100天 , 全靠阿里大牛的这份最全「高并发套餐」
之前我们对比了String、StringBuilder和StringBuffer的区别 , 其中一项便提到StringBuilder是非线程安全的 , 那么是什么原因导致了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来保证线程安全?如果不是用会出现什么异常情况?

下面我们来逐一讲解 。
我们先来跑一段代码示例 , 看看出现的结果是否与我们的预期一致 。
@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存在的意义 , 因为数组中的一部分内容为默认值 。
当调用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 codeif (minimumCapacity - value.length > 0) {value = http://news.hoteastday.com/a/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 codeint 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的线程不安全的原因?我们在学习和实践的过程中 , 不仅要知道一些结论 , 还要知道这些结论的底层原理 , 更重要的是学会分析底层原理的方法 。
作者:二师兄
原文链接:
【流光年华|为什么?为什么StringBuilder是线程不安全的?】


    推荐阅读