当前位置:   article > 正文

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

stringbuffer初始化 数组越界

参考:https://blog.csdn.net/u010982507/article/details/102381834

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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
  • 输出

    image-20200930092622958

问题1:为什么长度才 5387?

我们先来看 StringBuilder 的 append() 方法的源码:

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
  • 1
  • 2
  • 3
  • 4
  • 5

显然它调用了父类的 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

它在扩容的时候,没有引入 synchronized 关键字,并且执行了一条非常危险的语句 count += len,这条一句实际上是 count=count+len,它不是原子性的。所以当多个线程恰好同时执行该语句的话,那就会导致“实际 + 的次数少于应该 + 的次数”,从而导致最后的 length < 10000。

问题2:为什么会报 ArrayIndexOutOfBoundsException?

这是一个数组越界的问题,所以出现问题一定是在某次读取字符数组的时候,出现了问题。所以它会在哪里读字符数组呢?这里有一条语句str.getChars(0, len, value, count),我们来查查 JDK 文档看看对这个方法的说明:

image-20200930094137778

也就是说这个方法会对 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));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

它会先尝试看看原来的 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

扩容的时候将 value 的长度乘以2并加上2,然后再将变成后的 value[] 复制给原 value[],这里的 value[] 和 count 都是全局变量。

image-20200930102308538

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

image-20200930114240686

此处如有错误(基于 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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
  • 结果

    image-20200930101852653

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

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/1020271
推荐阅读
相关标签
  

闽ICP备14008679号