流光年华|为什么?为什么StringBuilder是线程不安全的?
- 周一福利到!献上“独家全新”MySQL进阶套餐 , 简直就是血赚
- 死磕「并发编程」100天 , 全靠阿里大牛的这份最全「高并发套餐」
本文插图
如果你看了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)9007StringBuilder中针对字符串的处理主要依赖两个成员变量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是线程不安全的?】
推荐阅读
- 兔子|兔兔这么可爱,为什么要吃屎?
- 埃及金字塔|探秘尼罗河|为什么会有人觉得,埃及金字塔是伪造的?
- 减肥|较真丨减肥产品含违禁药再被曝光,为什么说滥用这些产品是在玩命?
- 酿酒|用真全粮酿酒机器做酒,为什么发酵时间越长口感越好?
- 为什么商家卖的馒头又白又胖?里面加了什么东西,今天我来告诉你
- 慢性乙肝|慢性乙肝,为什么要等到转氨酶高,才抗病毒治疗?医生告诉你原因
- 保罗·狄拉克|为什么物质比反物质多?
- 月球|站在月球上眺望地球,为什么有人会觉得恐惧?他们看到了什么?
- 肾病|为什么肾病一发现就是晚期?
- 生殖隔离|黄种人、白种人、黑种人之间为什么没有生殖隔离?
