为什么说 StringBuilder 是线程不安全的且会发生数组越界问题而 StringBuffer 是线程安全的?

 

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());
    }
    
  • 输出

    为什么说 StringBuilder 是线程不安全的且会发生数组越界问题而 StringBuffer 是线程安全的?

问题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 文档看看对这个方法的说明:

为什么说 StringBuilder 是线程不安全的且会发生数组越界问题而 StringBuffer 是线程安全的?

也就是说这个方法会对 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 都是全局变量。

为什么说 StringBuilder 是线程不安全的且会发生数组越界问题而 StringBuffer 是线程安全的?

那么问题就来了,假如现在有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开始),这就发生了越界啦!

为什么说 StringBuilder 是线程不安全的且会发生数组越界问题而 StringBuffer 是线程安全的?

此处如有错误(基于 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());
    }
    
  • 结果

    为什么说 StringBuilder 是线程不安全的且会发生数组越界问题而 StringBuffer 是线程安全的?

没有一点问题,我们来看看 StringBuffer 的扩容机制:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

可以看出 SpringBuffer 的 append() 方法加了 synchronized 锁,保证的线程的安全(这也是 StringBuffer 效率比 StringBuilder 要低的原因)。


 

上一篇:Java面试题(5)String、StringBuffer、StringBuilder的区别


下一篇:Java字符串之StringBuffer和StringBuilder模拟栈