概述
我们都知道,StringBuffer 是(线程安全)的而 StringBuilder 是(非线程安全),并且当我们对字符串进行修改的时候,特别是字符串对象经常改变的情况下,建议使用 StringBuffer 和 StringBuilder 类。
StringBuilder 继承图
案例分析
首先通过一段代码去看一下多线程操作StringBuilder对象会出现什么问题?
public class StringBuilderTest {
/** 线程数 */
private static final int THREAD_NUMBER = 20;
private static final class StringBuilderThread implements Runnable{
private final StringBuilder buffer;
public StringBuilderThread(StringBuilder buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100 ; i++){
buffer.append("Y");
}
}
}
public static void main(String[] args) throws Exception {
StringBuilder buffer = new StringBuilder();
for (int i = 0 ; i< THREAD_NUMBER; i++) {
Thread t = new Thread(new StringBuilderThread(buffer));
t.start();
}
Thread.sleep(200);
System.out.println(buffer.length());
}
}
我们能看到这段代码 开了 20 个线程,分别循环100次向 StringBuilder 对象里面 append 字符串 Y 。正常情况下的代码 buffer 的长度应该为 2000 ,但是实际运行会输出什么呢?
-
结果一:没有报错,但是结果显然不对。
结果一.png -
结果二:报错,我们看到输出了“971”,小于预期的2000 ,并且还抛出了一个ArrayIndexOutOfBoundsException异常。
结果二.png
源码分析
通过看 StringBuilder、StringBuffer 和 String 的内部实现源码,它们都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。
我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)
从 StringBuilder 的 appned 方法 可以看出:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
调用 父类的 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;
}
可以看出 AbstractStringBuilder 中定义 两个特殊变量:
//存储字符串的具体内容
char[] value;
//已经使用的字符数组的数量
int count;
在 AbstractStringBuilder.append() 中:
知道 count += len 不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到 count += len 这句代码,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比2000 小的原因。
那为啥子还要报错(抛出ArrayIndexOutOfBoundsException异常)呢?
我们再看代码 方法: ensureCapacityInternal
private void ensureCapacityInternal(int minimumCapacity) {
// 判断长度是否是否大于数据的长度
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
继续看 expandCapacity 方法:
void expandCapacity(int minimumCapacity) {
// 扩容两倍
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
// 新数组
value = Arrays.copyOf(value, newCapacity);
}
扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
继续看 Arrays.copyOf 方法:
public static char[] copyOf(char[] original, int newLength) {
// new 新数组
char[] copy = new char[newLength];
// 将原数组的内容复制到新数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
// 返回新的char数组
return copy;
}
接下里看 这一句代码:
str.getChars(0, len, value, count);
是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面。
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);
}
具体分配图如下:
假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了ensureCapacityInternal()方法,此刻count=5。
这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了。
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。
以上就证明了 StringBuilder 是线程不安全的。如果我们将测试代码的StringBuilder对象换成StringBuffer对象会输出什么呢?
public class StringBufferTest {
private static final int THREAD_NUMBER = 20;
private static final class StringBufferThread implements Runnable{
private final StringBuffer buffer;
public StringBufferThread(StringBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100 ; i++){
buffer.append("Y");
}
}
}
public static void main(String[] args) throws Exception {
StringBuffer buffer = new StringBuffer();
for (int i = 0 ; i< THREAD_NUMBER; i++) {
Thread t = new Thread(new StringBufferThread(buffer));
t.start();
}
Thread.sleep(20);
System.out.println(buffer.length());
}
}
当然是输出2000啦!
那么StringBuffer用什么手段保证线程安全的?这个问题你点进StringBuffer的append()方法里面就知道了。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer 里面的所有方法毒采用的 synchronized 关键字来实现线程安全性。
结论
- 应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
- StringBuffe 相较于 StringBuffer 有速度优势,所以在不要求线程安全的情况下多数建议使用 StringBuilder 类。