可能大家在学习java的基础过程中,都知道StringBuilder相对StringBuffer效率更高,但是线程不安全。可是,到底是怎么个不安全法,反正我是懵的。借此机会,写个小代码测试下。
既然是测试StringBuilder和StringBuffer的线程安全性,那就分别new一个StringBuilder和StringBuffer作为共享变量,传递到多个线程进行操作,看看最后结果如何。
package com.cjt.test;
public class Test {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 10; i++) {
new A(builder, buffer).start();
}
}
}
class A extends Thread {
private StringBuilder builder;
private StringBuffer buffer;
A(StringBuilder builder, StringBuffer buffer) {
this.buffer = buffer;
this.builder = builder;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
builder.append("c");
buffer.append("c");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("[" + Thread.currentThread().getName() + "]builder:" + builder.length());
System.out.println("[" + Thread.currentThread().getName() + "]buffer:" + buffer.length());
}
}
我在run()
里面加了一个Thread.sleep(10)
延时,为了更好地体现多个线程操作同一个变量的安全性问题。运行结果:
[Thread-9]builder:939
[Thread-5]builder:939
[Thread-9]buffer:994
[Thread-5]buffer:994
[Thread-1]builder:941
[Thread-1]buffer:996
[Thread-4]builder:941
[Thread-4]buffer:996
[Thread-0]builder:941
[Thread-0]buffer:996
[Thread-8]builder:941
[Thread-8]buffer:996
[Thread-2]builder:942
[Thread-2]buffer:998
[Thread-6]builder:942
[Thread-6]buffer:998
[Thread-3]builder:944
[Thread-3]buffer:1000
[Thread-7]builder:944
[Thread-7]buffer:1000
说实话当我运行后也是一头问号?什么鬼,没有任何规律可寻。的确是这样,但是经过多次运行后可以总结一点是:StringBuffer最终的length总是1000,但是StringBuilder的length总是少于1000。
这也太没说服力了~!那就要分别看看它们的append()
源码了;
StringBuilder:
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer:
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
看样子好像就隔只一个synchronized
关键字,那就看看super.append(str)
的源码吧,但可以吃惊地发现都是AbstractStringBuilder.append(String str)
,看看这段代码:
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
由于底层字符串都是由char数组实现的,这里str.getChars(0, len, value, count);
就是一个明显的赋值操作。因而可以说StringBuilder和StringBuffer的append(String str)方法都是通过getChars方法来实现字符串拼接的。
有人看到上面还有个ensureCapacityInternal(count + len);
调用,这个只是一个底层的char数组扩容计算,有兴趣的可以自己私下看看源码,这里就不贴出来。
那我们就一探getChars方法的究竟:
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
if ((srcEnd < 0) || (srcEnd > count))
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
实质上是调用底层System.arraycopy()
方法实现的,可能你觉得我扯远了,其实,的确有点吧。
StringBuilder中的append方法没有使用synchronized关键字,意味着多个线程可以同时访问这个方法。那么问题就来了额,如果两个线程同时访问到这个方法,那么AbstractStringBuilder中的count是不是就是相同的,所以这两个线程都是在底层char数组的count位置开始append添加,那么最终的结果肯定就是在后执行的那个线程append进去的数据会将前面一个覆盖掉。因此我们的控制台输出才会出现StringBuilder一直都是小于1000的。然而StringBuffer却不会发生这种情况。
原文参考:https://www.jianshu.com/p/2cf1f60b0d9a