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