StringBuilder是线程不安全的原因是?

概述

我们都知道,StringBuffer 是(线程安全)的而 StringBuilder 是(非线程安全),并且当我们对字符串进行修改的时候,特别是字符串对象经常改变的情况下,建议使用 StringBuffer 和 StringBuilder 类。

StringBuilder 继承图

StringBuilder 继承图

案例分析

首先通过一段代码去看一下多线程操作StringBuilder对象会出现什么问题?

public class StringBuilderTest {

    /** 线程数 */
    private static final int THREAD_NUMBER = 20;
    
    private static final class StringBuilderThread implements Runnable{

        private final StringBuilder buffer;
        public StringBuilderThread(StringBuilder buffer) {
            this.buffer = buffer;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100 ; i++){
                buffer.append("Y");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        StringBuilder buffer = new StringBuilder();
        for (int i = 0 ; i< THREAD_NUMBER; i++) {
            Thread t = new Thread(new StringBuilderThread(buffer));
            t.start();
        }
        Thread.sleep(200);
        System.out.println(buffer.length());
    }
}

我们能看到这段代码 开了 20 个线程,分别循环100次向 StringBuilder 对象里面 append 字符串 Y 。正常情况下的代码 buffer 的长度应该为 2000 ,但是实际运行会输出什么呢?

  1. 结果一:没有报错,但是结果显然不对。


    结果一.png
  2. 结果二:报错,我们看到输出了“971”,小于预期的2000 ,并且还抛出了一个ArrayIndexOutOfBoundsException异常。


    结果二.png

源码分析

通过看 StringBuilder、StringBuffer 和 String 的内部实现源码,它们都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。

我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)

从 StringBuilder 的 appned 方法 可以看出:

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

调用 父类的 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;
    }

可以看出 AbstractStringBuilder 中定义 两个特殊变量:

//存储字符串的具体内容
char[] value;
//已经使用的字符数组的数量
int count;

在 AbstractStringBuilder.append() 中:

知道 count += len 不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到 count += len 这句代码,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比2000 小的原因。

那为啥子还要报错(抛出ArrayIndexOutOfBoundsException异常)呢?

我们再看代码 方法: ensureCapacityInternal

private void ensureCapacityInternal(int minimumCapacity) {
        // 判断长度是否是否大于数据的长度
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

继续看 expandCapacity 方法:

 void expandCapacity(int minimumCapacity) {
        // 扩容两倍
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
      
       // 新数组
        value = Arrays.copyOf(value, newCapacity);
    }

扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。

继续看 Arrays.copyOf 方法:

 public static char[] copyOf(char[] original, int newLength) {
        // new 新数组
        char[] copy = new char[newLength];
      // 将原数组的内容复制到新数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
    //  返回新的char数组
        return copy;
    }

接下里看 这一句代码:

str.getChars(0, len, value, count);

是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面。

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);
        }
      // 拷贝辅助数组
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

具体分配图如下:


StringBuilder 线程不安全说明.png

假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了ensureCapacityInternal()方法,此刻count=5。

StringBuilder 线程不安全说明.png

这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了。

StringBuilder 线程不安全说明.png

线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。

以上就证明了 StringBuilder 是线程不安全的。如果我们将测试代码的StringBuilder对象换成StringBuffer对象会输出什么呢?

public class StringBufferTest {

    private static final int THREAD_NUMBER = 20;
    
    private static final class StringBufferThread implements Runnable{

        private final StringBuffer buffer;

        public StringBufferThread(StringBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100 ; i++){
                buffer.append("Y");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        StringBuffer buffer = new StringBuffer();

        for (int i = 0 ; i< THREAD_NUMBER; i++) {
            Thread t = new Thread(new StringBufferThread(buffer));
            t.start();
        }
        Thread.sleep(20);
        System.out.println(buffer.length());
    }
}

当然是输出2000啦!

StringBuffer 执行结果.png

那么StringBuffer用什么手段保证线程安全的?这个问题你点进StringBuffer的append()方法里面就知道了。

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

StringBuffer 里面的所有方法毒采用的 synchronized 关键字来实现线程安全性。

结论

  1. 应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
  2. StringBuffe 相较于 StringBuffer 有速度优势,所以在不要求线程安全的情况下多数建议使用 StringBuilder 类。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。