前言
众所周知,StringBuilder是线程不安全的,StringBuffer是线程安全的。他们底层是怎么实现的呢?他们又为什么是线程不安全与线程安全的呢?今天我们从源码层面分析一下。
源码分析
StringBuilder和StringBuffer都是继承自AbstractStringBuilder这个抽象类,他们都重写了AbstractStringBuilder的append方法
StringBuilder
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer
@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
-
可以看出StringBuffer在append方法前面加了synchronized,把他变成了一个同步方法。线程之间需抢占,因此线程安全。但是效率也就低了。
-
再来看看是什么导致了StringBuilder的线程不安全。我们来进入父类AbstractStringBuilder,看看这个append
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,且必须是在程序开始时,数组不大的时候才可能出现这个异常。我试了好几次也没试出来。。。
- 这就是ensureCapacityInternal存在的线程安全性问题,我们再来看看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
测试StringBuffer
@Test
public void stringDemo03() throws InterruptedException {
CountDownLatch count=new CountDownLatch(10000);
StringBuffer stringBuffer=new StringBuffer();
for (int i = 0; i <100 ; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <100 ; j++) {
stringBuffer.append("q");
count.countDown();
}
}
});
t.start();
}
count.await();
System.out.println(stringBuffer.length());
}
10000