之前咱们比照了 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 来保障线程平安?如果不会呈现什么异常情况?
上面咱们来逐个解说。
异样示例
咱们先来跑一段代码示例,看看呈现的后果是否与咱们的预期统一。
@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。这种状况一旦产生 www.sangpi.com 便不会呈现预期的后果。
抛异样的起因
回头看异样的堆栈信息,发现有这么一行内容:
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 的线程不平安的起因?咱们在学习和实际的过程中,不仅要晓得一些论断,还要晓得这些论断的底层原理,更重要的是学会剖析底层原理的办法。