2

StringBuffer是同步的,但StringBuilder不是!这已在Difference between StringBuilder and StringBuffer深入讨论。究竟是什么原因导致StringBuilder在多线程环境中失败

有一个例子代码存在(由@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引用的源数组012h复制到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?!

+0

StringBuilder的是相同的StringBuffer,那就是它存储的对象堆,它也可以被修改。 StringBuffer和StringBuilder的主要区别在于StringBuilder不是线程安全的。 StringBuilder速度很快,因为它不是线程安全的。 – therealprashant

+0

@therealprashant当然!我已经要求了解详情,为什么会出错?! –

回答

3

这就是我的理解。你应该退一步,看看那里getChars是从AbstractStringBuilderappend方法叫:

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的大小,即要复制的目标索引大于数组的长度,导致调用IndexOutOfBoundsExceptionSystem.arraycopystr.getChars。在我们的做作的场景,count=5value大小为5,所以当System.arraycopy被调用,它无法复制到一个数组中的第6位这就是长5

2

如果你比较两个类,即StringBuilderStringBufferappend方法。你可以找到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

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

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

相关问题