java面试题之StringBuilder为什么不是线程安全的?

简介

StringStringBuilderStringBuffer是面试高频考察点,常常见到的经典面试题有下面这两种:

  1. String,StringBuilder的区别?

可变字符序列:StringBuilderStringBuffer是可变字符序列而String是不可变的字符序列,这一点我们可以通过源码可以得出这个结论:

String对象存储元素数组是不可变
在这里插入图片描述
StringBuilder与StringBuffer存储元素的数组是可变的
在这里插入图片描述
String与StringBuilder字符串拼接

下面等式在常量池中创建了3个分别是abab的char类型数组。

String c ="a"+"b";

下面这个等式创建了一个长度为16的字符数组,数组前三个元素分别是abc

StringBuilder sb = new StringBuilder();
sb.append("a").append("b").append("c");

String字符串每进行一次+拼接都会在内存中产生一个新的String类对象而StringBuilder仅仅只产生一个StringBuilder对象,所以在实际开发中遇到大量字符串拼接场景推荐使用StringBuilder

  1. StringBuilder,StringBuffer的区别?

我们都知道两者区别是StringBuffer有着更好的线程安全性这个结论,这一点我们可以参考两者源码方法上的区别如append方法:

StringBuilder

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

StringBuffer

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

可以很清晰看到StringBufferappend方法加了synchronized来保证线程安全,但是StringBuilder不安全性体现在那个方面呢?为了搞清楚这个问题让我们先看下面这段代码:

 public static void main(String[] args) throws InterruptedException {
        StringBuilder sb = new StringBuilder();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 10000; j++) {
                    sb.append("a");
                }
            });
        }
        // 这里休眠是让新建的线程尽可能执行完后才使用主线程去输出字符序列的长度
        Thread.sleep(300);
        System.out.println(sb.length());
        executorService.shutdown();
    }

上面的代码创建了容量10个线程的线程池,每一个线程使用StringBuilder对象调用其append方法去进行字符拼接。我们期待输出的是100000,但实际输出如下图所示:

扫描二维码关注公众号,回复: 11291187 查看本文章

在这里插入图片描述
可以很清晰看到输出的长度为20457且抛出了ArrayIndexOutOfBoundsException异常,下面从两个方面介绍这个原因:

为什么输出长度不是100000?

我们都知道StringBuilder对象底层使用的是可变的char类型数组,其append方法是调用了父类AbstractStringBuilder的append方法,两者append方法代码片段如下所示:

StringBuilder的append方法

	
    public StringBuilder append(StringBuffer sb) {
        super.append(sb);
        return this;
    }

AbstractStringBuilder的append方法
在这里插入图片描述
上面代码片段我们重点关注count +=len这一行语句实际包含了3步操作:
1.首先是获取变量countlen的值
2. 然后两者相加
3. 最后将和赋值给count变量

假设此时count值为1,len值为1。A、B两个线程都同时执行到了这一行语句,两个线程执行第一步操作获取count的值都为1,线程A执行了第二步操作,但是还没来得及执行第三步操作的时候,线程B也执行了第二步操作,这就造成了2个线程执行后的count值都为2而不是3。所以这也是上面测试的代码输出的值小于期望值100000的原因。

为什么会有ArrayIndexOutOfBoundsException异常?

AbstractStringBuilder的append方法分析如下:
ensureCapacityInternal方法源码分析

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        // 如果添加后的字符序列长度超出原有的数组长度则进行扩容操作
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
	// 扩容的方法
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
		// 一般扩展后的容量为原来的2倍再加2
        int newCapacity = (value.length << 1) + 2;
        // 此项判断如果扩容后的容量还是无法容纳新增的字符长度,就以新增字符长度作为扩容后数组的长度
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
		// 下面这个判断容量如果超出阈值则抛出异常,否则正常返回扩容后的容量
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }
    // 下面这个判断容量如果超出阈值则抛出异常,否则正常返回扩容后的容量
    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

ensureCapacityInternal是一个检查StringBuilder对象的char类型数组容量是否可以放入新的字符,如果放不了就会进行扩容的逻辑。
getChars()方法源码分析

   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);
        }
        // 将String类型的value数组元素拷贝到dst中
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

我们再来分析为什么会有异常的情况?
在这里插入图片描述
通过上面的分析我们知道append方法有下面2个步骤:
1 .每次添加元素会进行元素容量的检查,若容量不够则进行扩容操作。
2. 将被添加的字符串char数组拷贝到AbstractStringBuilder的数组中。

假设有A、B两个线程同时执行ensureCapacityInternal时,此时两个线程获取的count为15,AbstractStringBuilder的value数组长度为16。线程A占用的CPU权限被B线程获取,然后线程B执行完了整个append方法逻辑,然后count为16。线程A再次执行str.getchar方法此时count为16,然后在从AbstractStringBuilder的数组索引为索引16的位置上拷贝一个a,而数组索引最大值为15,所以就有了ArrayIndexOutOfBoundsException异常。

猜你喜欢

转载自blog.csdn.net/javaee_gao/article/details/100172493