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 类。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356