StringBuilder在多线程环境中失败的真正原因

时间:2015-07-13 05:07:23

标签: java multithreading synchronization thread-safety stringbuilder

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.class getChars方法调用System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);根据System.arraycopy javadoc:< / p>

  

从指定的源数组复制数组,从   指定的位置,到目的地的指定位置   阵列。从源复制数组组件的子序列   src引用的数组到dest引用的目标数组。   复制的组件数等于length参数。   ....

     

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

2 个答案:

答案 0 :(得分:3)

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

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"),它将调用ensureCapacityInternalvalue足够大,因此不会调整大小(需要大小为5)。线程1暂停。

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

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

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

答案 1 :(得分: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"时。

如果是 StringBuilder ensureCapacityInternal()同时从不同的线程调用。这导致基于调用中的先前值更改大小,之后两个线程都追加"some string"导致ArrayIndexOutOfBoundsException

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

对于 StringBuffer ,append方法已经同步,因此不会出现这种情况。