一 背景
我们熟悉String StringBuilder StringBuffer 使用特性以及安全特性;
- String:不可变;
- StringBuilder:线程不安全;
- StringBuffer:线程安全。
可是对于StringBuilder为什么是线程不安全的可能没有做过相关的分析,它为什么说是线程不安全的这里进行简单介绍。
二 测试
具体分析之前我们先看下多个线程同时操作StringBuilder 会有什么情况发生?
具体示例代码如下:
private static void test() throws Exception {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
builder.append("a");
}
}).start();
}
Thread.sleep(1000);
System.out.println(builder.length());
}
运行结果:
我们期望的运行结果是10个线程对其进行append操作,每个线程进行1000次,所以最终结果应该是1000*10,但是多次运行的情况如下:
- 运行情况1:length每次都是小于1000*10,
- 运行情况2:出现ArrayIndexOutOfBoundsException异常;
第一次运行:
6900
第二次运行:
Exception in thread "Thread-5" 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.mzw.base.java.string.StringTest$1.run(StringTest.java:20)
at java.lang.Thread.run(Thread.java:748)
4949
...
在分析设个问题之前,我们多少需要了解StringBuilder和StringBuffer的内部实现。StringBuilder和StringBuffer的内部实现其实跟String类一样,都是通过一个char数组存储字符串的(见下面介绍), 不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。
String 内部结构
- 内部维护一个不可变的char 数组;
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
StringBuilder 内部结构
- StringBuilder显式的继承了AbstractStringBuilder,StringBuilder的构造器显示的调用了父类AbstractStringBuilder的构造器,AbstractStringBuilder内部维护了一个可变的char 数组;
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
public StringBuilder() {
super(16);
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
...
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
public synchronized StringBuffer append(StringBuffer sb) {
toStringCache = null;
super.append(sb);
return this;
}
}
StringBuffer 内部结构
- StringBuffer 同样也是显式的继承了AbstractStringBuilder,所以这里不再给出其源码结构的展示;StringBuffer内部提供的对AbstractStringBuilder维护的可变的char 数组的操作方法都是同步的,所以他是线程安全的。
分析
了解String StringBuilder StringBuffer 内部结构之后,我们了解StringBuilder的append操作其实就是现实的调用了其父类AbstractStringBuilder的append操作。
- StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
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;
}
}
运行情况1 原因
- 这里很明显看到其对count的操作(The count is the number of characters used.),count += len 多线程下该操作由于并不是一个原子操作,所以其值可能会出现小于预期值的情况,这也是我们执行情况每次都是小于1000*10的原因;
运行情况2 原因
同样分析上面的append方法:
- 第一步:int len = str.length(); 获取append字符串的长度;
- 第二步:ensureCapacityInternal(count + len);检查StringBuilder对象的原char数组的容量能不能装得下新的字符串,如不能则对char数组进行扩容。
- 第三步:str.getChars(0, len, value, count);将字符从字符串复制到目标字符数组。
- 第四部:count += len; 对char的count进行计算;
这里简单分析下ensureCapacityInternal方法:
- 扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
具体分析下str.getChars(0, len, value, count);方法,
- getChars() 方法将字符从字符串复制到目标字符数组。
语法:
- public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
参数:
- srcBegin -- 字符串中要复制的第一个字符的索引。
- srcEnd -- 字符串中要复制的最后一个字符之后的索引。
- dst -- 目标数组。
-
dstBegin -- 目标数组中的起始偏移量。
方法最终调用System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin)
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
...//清除检查代码
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
举例:
- dst(count=5)【a】【b】【c】【d】【】
- 现两个线程A(e).B(f) 同时执行appengd方法:假设两个线程都已经同时执行完ensureCapacityInternal方法,比如此刻的count=5;
- 如果此时线程A或线程B继续执行,某一个线程(假设是线程A)执行完成整个的appengd方法后count = 6; 即【a】【b】【c】【d】【e】
- 此时线程B继续执行,当线程执行到getChars方法拿到的值count值是6,数组已经放满数据了不能再容纳新的数据,由于线程B已经执行完了ensureCapacityInternal方法,数组不在进行判断是否需要扩容,所以在执行数组拷贝就会出现ArrayIndexOutOfBoundsException了。
网友评论