PS:上述参考的博主写的非常好,但是笔者在分析数组越界问题上与其有些许不同的见解,还请批评指导。
1. StringBuilder
-
测试 StringBuilder
public static void main(String[] args) { StringBuilder sb = new StringBuilder(); //创建10个线程 for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { //每个线程循环1000次 for (int j = 0; j < 1000; j++) { sb.append("a"); } } }).start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //按道理10*1000=10000 System.out.println("长度本来应该是10000的,现在是:"+sb.length()); }
-
输出
问题1:为什么长度才 5387?
我们先来看 StringBuilder 的 append() 方法的源码:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
显然它调用了父类的 append() 方法,我们再来看看其父类的 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;
}
它在扩容的时候,没有引入 synchronized 关键字,并且执行了一条非常危险的语句 count += len
,这条一句实际上是 count=count+len
,它不是原子性的。所以当多个线程恰好同时执行该语句的话,那就会导致“实际 + 的次数少于应该 + 的次数”,从而导致最后的 length < 10000。
问题2:为什么会报 ArrayIndexOutOfBoundsException?
这是一个数组越界的问题,所以出现问题一定是在某次读取字符数组的时候,出现了问题。所以它会在哪里读字符数组呢?这里有一条语句str.getChars(0, len, value, count)
,我们来查查 JDK 文档看看对这个方法的说明:
也就是说这个方法会对 value 从 value[count] 开始插入 len 个来自 str 的字符,这里的 count 是指 StringBuilder 中有效的字符数。那么问题就来了,如果在插入 len 个字符的时候,超过 value[] 的容量呢?比如 value 的长度为10,然后我们从 value[6] 开始插入数据,但是 len =10,那么在插入后面的数据肯定会发生数据越界问题,比如我们企图插入 value[12],就发生了越界!
回到 append() 方法,这是一个拼接字符串的方法,那么就涉及到 StringBuilder 的扩容问题了。append() 方法中的 ensureCapacityInternal(count + len)
语句就是扩容语句,我们再来看看 ensureCapacityInternal()
方法:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
它会先尝试看看原来的 value[] 能否放下新的字符串,如果放得下就轻松了,放不下的话就调用了 newCapacity()
方法进行扩容:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
扩容的时候将 value 的长度乘以2并加上2,然后再将变成后的 value[] 复制给原 value[],这里的 value[] 和 count 都是全局变量。
那么问题就来了,假如现在有2个线程同时执行了 append() 方法中的 ensureCapacityInternal(count + len)
语句,第1个线程不需要扩容的(假设 count=100,capacity=150,len1=45),所以它准备继续执行 str.getChars(0, len, value, count)
方法。这个时候第2个线程也来执行 ensureCapacityInternal(count + len)
语句(这个时候count=100,capacity=150,len2=160),并且它认为需要扩容,然后调用了 newCapacity()
方法进行扩容使得 capacity 变成了 150*2+2 = 302 ,然后第2个线程完成了 count += len
,使得 count = 260。关键来了!这个时候(count=260,capacity=302,len1=45)第1个线程要执行 str.getChars(0, len, value, count)
方法,这个时候就出问题了,value[] 的容量只是302,而这个时候count已经变成260了,我们需要插入45个长度的字符,所以这个时候 302<260+50,也就是说我们会访问到 value[302](数组从0开始),这就发生了越界啦!
此处如有错误(基于 JDK8.0),请批评指出!
2. StringBuffer
-
测试 StringBuffer
public static void main(String[] args) { StringBuffer sb = new StringBuffer(); //生成10个线程 for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { //每个线程执行1000次 for (int j = 0; j < 1000; j++) { sb.append("a"); } } }).start(); } //让上面执行完 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //按道理应该 10*1000=10000 System.out.println("本来长度应该是10000,现在是:"+sb.length()); }
-
结果
没有一点问题,我们来看看 StringBuffer 的扩容机制:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
可以看出 SpringBuffer 的 append() 方法加了 synchronized
锁,保证的线程的安全(这也是 StringBuffer 效率比 StringBuilder 要低的原因)。