美文网首页
StringBuilder 线程不安全分析

StringBuilder 线程不安全分析

作者: 花神子 | 来源:发表于2019-12-11 17:17 被阅读0次

    一 背景

    我们熟悉String StringBuilder StringBuffer 使用特性以及安全特性;

    • String:不可变;
    • StringBuilder:线程不安全;
    • StringBuffer:线程安全。
      可是对于StringBuilder为什么是线程不安全的可能没有做过相关的分析,它为什么说是线程不安全的这里进行简单介绍。

    二 测试

    具体分析之前我们先看下多个线程同时操作StringBuilder 会有什么情况发生?

    具体示例代码如下:

    private static void test() throws Exception {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    builder.append("a");
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(builder.length());
    }
    

    运行结果:

    我们期望的运行结果是10个线程对其进行append操作,每个线程进行1000次,所以最终结果应该是1000*10,但是多次运行的情况如下:

    1. 运行情况1:length每次都是小于1000*10,
    2. 运行情况2:出现ArrayIndexOutOfBoundsException异常;
    第一次运行:
    6900
    
    第二次运行:
    Exception in thread "Thread-5" java.lang.ArrayIndexOutOfBoundsException
        at java.lang.System.arraycopy(Native Method)
        at java.lang.String.getChars(String.java:826)
        at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449)
        at java.lang.StringBuilder.append(StringBuilder.java:136)
        at com.mzw.base.java.string.StringTest$1.run(StringTest.java:20)
        at java.lang.Thread.run(Thread.java:748)
    4949
    
    ...
    

    在分析设个问题之前,我们多少需要了解StringBuilder和StringBuffer的内部实现。StringBuilder和StringBuffer的内部实现其实跟String类一样,都是通过一个char数组存储字符串的(见下面介绍), 不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。

    String 内部结构

    • 内部维护一个不可变的char 数组
    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
      ...
    }
    

    StringBuilder 内部结构

    • StringBuilder显式的继承了AbstractStringBuilder,StringBuilder的构造器显示的调用了父类AbstractStringBuilder的构造器,AbstractStringBuilder内部维护了一个可变的char 数组
    public final class StringBuilder
        extends AbstractStringBuilder
        implements java.io.Serializable, CharSequence {
        
        public StringBuilder() {
          super(16); 
        }
        
        @Override
        public StringBuilder append(String str) {
            super.append(str);
            return this;
        }    
        ...
    }
    
    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        /**
         * The value is used for character storage. 
         */  
         char[] value;
         
         public synchronized StringBuffer append(StringBuffer sb) {
            toStringCache = null;
            super.append(sb);
            return this;
        }
    }
    

    StringBuffer 内部结构

    • StringBuffer 同样也是显式的继承了AbstractStringBuilder,所以这里不再给出其源码结构的展示;StringBuffer内部提供的对AbstractStringBuilder维护的可变的char 数组的操作方法都是同步的,所以他是线程安全的。

    分析

    了解String StringBuilder StringBuffer 内部结构之后,我们了解StringBuilder的append操作其实就是现实的调用了其父类AbstractStringBuilder的append操作。

    • StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法:
    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        
        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;
        }
    }
    

    运行情况1 原因

    • 这里很明显看到其对count的操作(The count is the number of characters used.),count += len 多线程下该操作由于并不是一个原子操作,所以其值可能会出现小于预期值的情况,这也是我们执行情况每次都是小于1000*10的原因;

    运行情况2 原因

    同样分析上面的append方法:

    • 第一步:int len = str.length(); 获取append字符串的长度;
    • 第二步:ensureCapacityInternal(count + len);检查StringBuilder对象的原char数组的容量能不能装得下新的字符串,如不能则对char数组进行扩容。
    • 第三步:str.getChars(0, len, value, count);将字符从字符串复制到目标字符数组。
    • 第四部:count += len; 对char的count进行计算;

    这里简单分析下ensureCapacityInternal方法:

    • 扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
    

    具体分析下str.getChars(0, len, value, count);方法,

    • getChars() 方法将字符从字符串复制到目标字符数组。

    语法:

    • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)

    参数:

    • srcBegin -- 字符串中要复制的第一个字符的索引。
    • srcEnd -- 字符串中要复制的最后一个字符之后的索引。
    • dst -- 目标数组。
    • dstBegin -- 目标数组中的起始偏移量。
      方法最终调用System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin)
    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        ...//清除检查代码
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }
    

    举例:

    1. dst(count=5)【a】【b】【c】【d】【】
    2. 现两个线程A(e).B(f) 同时执行appengd方法:假设两个线程都已经同时执行完ensureCapacityInternal方法,比如此刻的count=5;
    3. 如果此时线程A或线程B继续执行,某一个线程(假设是线程A)执行完成整个的appengd方法后count = 6; 即【a】【b】【c】【d】【e】
    4. 此时线程B继续执行,当线程执行到getChars方法拿到的值count值是6,数组已经放满数据了不能再容纳新的数据,由于线程B已经执行完了ensureCapacityInternal方法,数组不在进行判断是否需要扩容,所以在执行数组拷贝就会出现ArrayIndexOutOfBoundsException了。

    相关文章

      网友评论

          本文标题:StringBuilder 线程不安全分析

          本文链接:https://www.haomeiwen.com/subject/zacagctx.html