StringBuilder和StringBuffer相似,两个类的构造器和方法也基本相同。不同的是StringBuffer是线程安全的,StringBuilder没有实现线程安全功能,所以性能略高。StringBuffer类中的方法都添加了synchronized关键字,给方法添加了一个锁,用来保证线程安全。Java9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。在Java9以前字符串采用char[]数组来保存字符,字符串的每个字符占2字节,Java9的字符串采用byte[]数组再加一个encoding-flag字段来保存字符,字符串的每个字符只占1字节,所以Java9的字符串更加节省空间。
StringBuilder源码
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
是什么导致了StringBuilder的线程不安全,进入父类AbstractStringBuilder
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
问题在这两行
ensureCapacityInternal(count + len);
putStringAt(count, str);
ensureCapacityInternal() 是一个扩容方法
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity) << coder);
}
}
coder是字符的编码格式,编码默认是Latin1,对应的coder是0。还有一种编码UTF16,对应的coder为1。用count(已经使用的长度)+len(要拼接的长度)得到需要的最小长度minimumCapacity。如果这个长度比原来的容量大,则触发扩容,把原数组复制到一个容量为(minimumCapacity*2+2)的新数组。在并发情况下,可能有多个线程拿到相同count,导致扩容不充分引起数组下标越界异常。一般要三个以上线程同时拿到count且必须是在程序开始时,数组不大的时候才可能出现这个异常。再来看putStringAt():
private final void putStringAt(int index, String str) {
if (getCoder() != str.coder()) {
inflate();
}
str.getBytes(value, index, coder);
}
在多线程下,可能有多个线程拿到相同count,在执行getBytes时,这几个线程添加的位置是相同的,可能会发生数据覆盖的情况。StringBuilder线程安全测试示例:
@Test
public void stringDemo02() throws InterruptedException {
CountDownLatch count=new CountDownLatch(10000);
StringBuilder stringBuilder=new StringBuilder();
for (int i = 0; i <100 ; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <100 ; j++) {
stringBuilder.append("q");
count.countDown();
}
}
});
t.start();
}
count.await();
System.out.println(stringBuilder.length());
}
9912