StringBuilder和StringBuffer的线程安全性的源码级理解(JDK14)

前言

众所周知,StringBuilder是线程不安全的,StringBuffer是线程安全的。他们底层是怎么实现的呢?他们又为什么是线程不安全与线程安全的呢?今天我们从源码层面分析一下。

源码分析

StringBuilder和StringBuffer都是继承自AbstractStringBuilder这个抽象类,他们都重写了AbstractStringBuilder的append方法

StringBuilder

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

StringBuffer

    @Override
    @HotSpotIntrinsicCandidate
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
  • 可以看出StringBuffer在append方法前面加了synchronized,把他变成了一个同步方法。线程之间需抢占,因此线程安全。但是效率也就低了。

  • 再来看看是什么导致了StringBuilder的线程不安全。我们来进入父类AbstractStringBuilder,看看这个append

    public AbstractStringBuilder append(String str) {
        if (str == null) {
            return appendNull();
        }
        int len = str.length();
        ensureCapacityInternal(count + len);
        putStringAt(count, str);
        count += len;
        return this;
    }

问题出在这两行

ensureCapacityInternal(count + len);
putStringAt(count, str); 

ensureCapacityInternal() 这是一个扩容方法。

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        int oldCapacity = value.length >> coder;
        if (minimumCapacity - oldCapacity > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity) << coder);
        }
    }
  • coder是字符的编码格式,编码默认是Latin1,对应的coder是0。还有一种编码UTF16,对应的coder为1一般不需要考虑他。可以看到用count(已经使用的长度)+len(要拼接的长度)得到需要的最小长度minimumCapacity。如果这个长度比原来的容量大,则触发扩容,把原数组复制到一个容量为(minimumCapacity*2+2)的新数组,并赋值给原数组。
  • 在并发情况下,可能有多个线程拿到相同count,从而导致扩容不充分,引起数组下标越界异常。由于扩容到两倍了,所以一般要三个以上线程同时拿到count,且必须是在程序开始时,数组不大的时候才可能出现这个异常。我试了好几次也没试出来。。。
  • 这就是ensureCapacityInternal存在的线程安全性问题,我们再来看看putStringAt()
    private final void putStringAt(int index, String str) {
        if (getCoder() != str.coder()) {
            inflate();
        }
        str.getBytes(value, index, coder);
    }

在多线程下,可能有多个线程拿到相同count,在执行getBytes时,这几个线程添加的位置是相同的,相当于相互覆盖了。使得一些线程做了工作,工作成果却被别人覆盖了,太气了。

测试

测试StringBuilder

    @Test
    public void stringDemo02() throws InterruptedException {
        CountDownLatch count=new CountDownLatch(10000);
        StringBuilder stringBuilder=new StringBuilder();
        for (int i = 0; i <100 ; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j <100 ; j++) {
                        stringBuilder.append("q");
                        count.countDown();
                    }
                }
            });
            t.start();
        }
        count.await();
        System.out.println(stringBuilder.length());
    }
9912

测试StringBuffer

    @Test
    public void stringDemo03() throws InterruptedException {
        CountDownLatch count=new CountDownLatch(10000);
        StringBuffer stringBuffer=new StringBuffer();
        for (int i = 0; i <100 ; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j <100 ; j++) {
                        stringBuffer.append("q");
                        count.countDown();
                    }
                }
            });
            t.start();
        }
        count.await();
        System.out.println(stringBuffer.length());
    }
10000
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值