为什么StringBuilder线程不安全,但StringBuffer线程安全?

StringBuilder和StringBuffer都继承了AbstractStringBuilder,AbstractStringBuilder内有两个非常重要的变量,分别是:

// 用于存储字符的容器
char[] value; 

// 已存储的字符数量
int count; 

注意,value没有加上final,也就是说,它是可变的,这点与String不同。

StringBuilder与StringBuffer自身的append()方法内都使用了继承自AbstractStringBuilder的append()方法。既然使用同一个方法,那为什么说StringBuilder线程不安全呢?

原因在于StringBuffer在append()方法上使用了synchronized,而StringBuilder没有使用。

以下是来自AbstractStringBuilder的append()方法的具体实现:

1    public AbstractStringBuilder append(String str) {
2        if (str == null)
3            return appendNull();
4        int len = str.length();              // 待插入的字符串的长度
5        ensureCapacityInternal(count + len); // 扩容
6        str.getChars(0, len, value, count);  // 将str拷贝到数组中
7        count += len;    // 容器内实际容纳的字符长度 = 原有长度 + 本次新增的长度
8        return this;
9    }

由String提供的getChars()方法

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        // ... 校验语句
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

其中,System.arraycopy可以翻译成如下解释:

把value数组从srcBegin开始,将srcEnd-srcBegin个元素,复制到dstBegin数组从dstBegin的位置之后。

如果配合append()当中的代码,还可以把上述解释进一步的翻译:

把str字符串对应的字符数组从0开始,将str长度个数的元素,复制到当前容器的value数组中从最后一个实际元素的位置之后。

当没有对append()方法加锁时,多线程并发下至少有以下三个场景可能导致程序出现错误:

场景1

第7行语句使用了"+="。

假设线程1将count值加载到寄存器并进行了加一运算,但尚未来得及将结果写回到内存。不巧此时线程2做完了count值加载到寄存器->加一运算->回写内存的全套操作,那么等到线程1最终反写结果至内存后,我们会发现,虽然两个线程分别做了1次加一运算,但实际count相当于只做了1次加一运算。而无论是StringBuilder还是StringBuffer在toString()时,都用到了count变量,这就导致最终输出的字符串长度可能会小于期望的字符串长度。

场景2

请看第5行语句,ensureCapacityInternal()的作用是扩容。假设有两个线程同时调用了append(),恰好当前容器不足以容纳新字符,此时会执行扩容操作。假设线程1和线程2都扩容完毕,线程1分配到的CPU时间片用完,线程2抓住机会执行完第6行语句(还没来得及执行第7行语句,count没有发生变化!)。

注意:此时第6行语句执行完毕,线程2需要新增的字符串已经成功的写入了扩容后的字符数组(容器)中了。此时线程1被唤醒,继续执行剩下的语句,由于count没有发生变化,导致线程1执行str.getChars()复制字符的起始位置没有发生变化,因此线程1将覆盖线程2刚刚写入的数据。如下图所示,假设容器内原有a和b两个字符,线程2从第3个位置开始复制,新增了c和d,线程1同样是从第3个位置开始复制,新增了f、g、h,显然把c和d给覆盖了。

场景3

与场景2一样,同样假设线程1和线程2都扩容完毕。线程1分配到的CPU时间片用完,线程2抓住机会执行完整个append()方法。这时count和value一定会发生变化,并且能够被线程1感知到。

线程1被唤醒,它在执行第六行语句时,使用的是被线程2修改后的count,换句话说,复制数据到当前容器的value[]数组的起点向后推移了(变大了)。

能插入数据的数组长度 = 容器的长度 - count

容器(即使是扩容后的容器)的长度是固定的,扩容的目的就是为了确保容器有足够的空间插入新数据,现在count变大了,那么能插入数据的空间就会变小,线程1在插入数据时就有可能因为数组没有那么大的剩余空间插入数据,导致抛出ArrayIndexOutOfBoundsException

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值