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方法已經同步,所以這種情況不會出現。

相關問題