【问题标题】:What actual cause the StringBuilder fails in multi threading environmentStringBuilder 在多线程环境中失败的实际原因是什么
【发布时间】:2015-07-13 05:07:22
【问题描述】:

StringBuffer 是同步的,但 StringBuilder 不是!这已在Difference between StringBuilder and StringBuffer 进行了深入讨论。

那里有一个示例代码(由@NicolasZozol 回答),它解决了两个问题:

  • 比较这些StringBufferStringBuilder 的性能
  • 表明StringBuilder 在多线程环境中可能会失败。

我的问题是关于第二部分,到底是什么让它出错了?! 当您运行代码几次时,堆栈跟踪显示如下:

Exception in thread "pool-2-thread-2" java.lang.ArrayIndexOutOfBoundsException
    at java.lang.String.getChars(String.java:826)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:416)
    at java.lang.StringBuilder.append(StringBuilder.java:132)
    at java.lang.StringBuilder.append(StringBuilder.java:179)
    at java.lang.StringBuilder.append(StringBuilder.java:72)
    at test.SampleTest.AppendableRunnable.run(SampleTest.java:59)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:722)

当我追踪代码时,我发现实际抛出异常的类是:String.classgetChars 调用System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); 的方法根据System.arraycopy javadoc:

从指定的源数组复制一个数组,从 指定位置,到目的地的指定位置 大批。从源复制数组组件的子序列 src 引用的数组到 dest 引用的目标数组。 复制的组件数量等于长度参数。 ....

IndexOutOfBoundsException - 如果复制会导致访问数据 超出数组范围。

为简单起见,我将代码完全粘贴在这里:

public class StringsPerf {

    public static void main(String[] args) {

        ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
        //With Buffer
        StringBuffer buffer = new StringBuffer();
        for (int i = 0 ; i < 10; i++){
            executorService.execute(new AppendableRunnable(buffer));
        }
        shutdownAndAwaitTermination(executorService);
        System.out.println(" Thread Buffer : "+ AppendableRunnable.time);

        //With Builder
        AppendableRunnable.time = 0;
        executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
        StringBuilder builder = new StringBuilder();
        for (int i = 0 ; i < 10; i++){
            executorService.execute(new AppendableRunnable(builder));
        }
        shutdownAndAwaitTermination(executorService);
        System.out.println(" Thread Builder: "+ AppendableRunnable.time);

    }

   static void shutdownAndAwaitTermination(ExecutorService pool) {
        pool.shutdown(); // code reduced from Official Javadoc for Executors
        try {
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                pool.shutdownNow();
                if (!pool.awaitTermination(60, TimeUnit.SECONDS))
                    System.err.println("Pool did not terminate");
            }
        } catch (Exception e) {}
    }
}

class AppendableRunnable<T extends Appendable> implements Runnable {

    static long time = 0;
    T appendable;
    public AppendableRunnable(T appendable){
        this.appendable = appendable;
    }

    @Override
    public void run(){
        long t0 = System.currentTimeMillis();
        for (int j = 0 ; j < 10000 ; j++){
            try {
                appendable.append("some string");
            } catch (IOException e) {}
        }
        time+=(System.currentTimeMillis() - t0);
    }
}

您能否更详细地描述(或使用示例)以显示多线程如何导致System.arraycopy 失败,?!或者线程如何使invalid data 传递给System.arraycopy?!

【问题讨论】:

  • StringBuilder 和 StringBuffer 一样,就是把对象存放在堆中,也可以修改。 StringBuffer 和 StringBuilder 的主要区别在于 StringBuilder 不是线程安全的。 StringBuilder 速度很快,因为它不是线程安全的。
  • @therealprashant 当然!!我已经询问了详细信息,为什么会出错?!

标签: java multithreading synchronization thread-safety stringbuilder


【解决方案1】:

我是这样理解的。你应该退后一步,看看getCharsAbstractStringBuilderappend方法中的调用位置:

public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

ensureCapacity 方法将检查属性value 是否足够长以存储附加值,如果没有,它将相应地调整大小。

假设 2 个线程在同一个实例上调用此方法。请记住,valuecount 由两个线程访问。在这个人为的场景中,假设 value 是一个大小为 5 的数组,并且数组中有 2 个字符,所以 count=2(如果您查看 length 方法,您会看到它返回 count)。

线程 1 调用 append("ABC") 将调用 ensureCapacityInternal 并且 value 足够大,因此不会调整大小(需要大小 5)。线程 1 暂停。

线程 2 调用 append("DEF") 将调用 ensureCapacityInternal 并且 value 足够大,因此也不会调整大小(也需要大小 5)。线程 2 暂停。

线程 1 继续并毫无问题地调用 str.getChars。然后它调用count += len。线程 1 暂停。请注意,value 现在包含 5 个字符,长度为 5。

线程 2 现在继续并调用 str.getChars。请记住,它使用与线程 1 相同的value 和相同的count。但是现在,count 已经增加并且可能大于value 的大小,即要复制的目标索引大于在str.getChars 中调用System.arraycopy 时导致IndexOutOfBoundsException 的数组。在我们设计的场景中,count=5value 的大小为 5,因此当调用 System.arraycopy 时,它无法复制到长度为 5 的数组的第 6 位。

【讨论】:

    【解决方案2】:

    如果您比较两个类中的append 方法,即StringBuilderStringBuffer。您会发现StringBuilder.append()不同步,而StringBuffer.append()同步

    // StringBuffer.append
    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }
    
    // StringBuilder.append
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    

    因此,当您尝试使用多个线程追加 "some string" 时。

    如果是 StringBuilderensureCapacityInternal() 同时被不同的线程调用。这会导致大小根据两个调用中的先前值发生变化,之后两个线程都附加"some string" 导致ArrayIndexOutOfBoundsException

    例如: 字符串值是“一些字符串”。现在 2 个线程想要附加“一些字符串”。所以两者都会调用ensureCapacityInternal() 方法,如果没有足够的空间,这将导致长度增加,但如果还有 11 个位置,那么它不会增加大小。现在两个线程同时用“一些字符串”调用了System.arraycopy。然后两个线程都尝试附加“一些字符串”。所以实际增加的长度应该是22,但是char[]里面有11个空位,导致ArrayIndexOutOfBoundsException。

    如果是StringBuffer,append方法已经同步,不会出现这种情况。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-16
      相关资源
      最近更新 更多