StringBuilder 在多线程环境中失败的实际原因

What actual cause the StringBuilder fails in multi threading environment

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

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

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

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)

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

Copies an array from the specified source array, beginning at the specified position, to the specified position of the destination array. A subsequence of array components are copied from the source array referenced by src to the destination array referenced by dest. The number of components copied is equal to the length argument. ....

IndexOutOfBoundsException - if copying would cause access of data outside array bounds.

为简单起见,我将代码准确地粘贴在这里:

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 ?!

如果您比较 类 中的 append 方法,即 StringBuilderStringBuffer。你可以发现 StringBuilder.append() is not synchronized where as StringBuffer.append()synchronized.

// 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.

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

StringBuffer的情况下,append方法已经同步,所以不会出现这种情况。

我是这样理解的。您应该退后一步,看看在 AbstractStringBuilder append 方法中调用 getChars 的位置:

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 方法,你会发现它 returns 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 个位置。