爲何StringBuilder是線程不安全的?

面試中常常問到的一個問題:StringBuilderStringBuffer的區別是什麼?咱們很是自信的說出:StringBuilder是線程不安全的,StirngBuffer是線程安全的面試官:StringBuilder不安全的點在哪兒?這時候估計就啞吧了。。。 面試

分析

StringBufferStringBuilder的實現內部是和String內部同樣的,都是經過 char[]數組的方式;不一樣的是Stringchar[]數組是經過final關鍵字修飾的是不可變的,而StringBufferStringBuilderchar[]數組是可變的。數組

首先咱們看下邊這個例子:安全

public class Test {
    public static void main(String[] args) throws InterruptedException {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 10000; i++){
            new Thread(() -> {
                for (int j = 0; j < 1000; j++){
                    stringBuilder.append("a");
                }
            }).start();
        }

        Thread.sleep(100L);
        System.out.println(stringBuilder.length());
    }
}複製代碼

直覺告訴咱們輸出結果應該是10000000,可是實際運行結果並不是咱們所想。app

執行結果

從上圖能夠看到輸出結果是9970698,並不是是咱們預期的1000000,而且還拋出了一個異常ArrayIndexOutOfBoundsException{非必現}函數

爲何輸出結果並不是預期值?

咱們先看一下StringBuilder的兩個成員變量(這兩個成員變量其實是定義在AbstractStringBuilder裏面的,StringBuilderStringBuffer都繼承了AbstractStringBuilder測試

AbstractStringBuilder.class

StringBuilderappend方法ui

StringBuilder.append(String str)

StringBuilderappend方法調用了父類的append方法spa

AbstractStringBuilder.append(String str)

咱們直接看第七行代碼,count += len; 不是一個原子操做,實際執行流程爲線程

  • 首先加載count的值到寄存器
  • 在寄存器中執行 +1操做
  • 將結果寫入內存

假設咱們count的值是10len的值爲1,兩個線程同時執行到了第七行,拿到的值都是10,執行完加法運算後將結果賦值給count,因此兩個線程最終獲得的結果都是11,而不是12,這就是最終結果小於咱們預期結果的緣由。3d

爲何會拋出ArrayIndexOutOfBoundsException異常?

咱們看回AbstractStringBuilder的追加()方法源碼的第五行,ensureCapacityInternal()方法是檢查StringBuilder的對象的原字符數組的容量能不能盛下新的字符串,若是盛不下就調用expandCapacity()方法對字符數組進行擴容。

private  void  ensureCapacityInternal(int  minimumCapacity)  {
         //溢出意識代碼
    if  (minimumCapacity  -  value .length>  0)
        expandCapacity(minimumCapacity); 
}複製代碼

擴容的邏輯就是新一個新的字符數組,新的字符數組的容量是原來字符數組的兩倍再加2,再經過System.arryCopy()函數將原數組的內容複製到新數組,最後將指針指向新的字符數組。

void  expandCapacity(int  minimumCapacity)  {
     //計算新的容量
    int  newCapacity =  value .length *  2  +  2 ; 
    //中間省略了一些檢查邏輯
     ...
     value  = Arrays.copyOf( value,newCapacity); 
}複製代碼

Arrys.copyOf()方法

public  static  char []  copyOf(char [] original,  int  newLength)  {
     char [] copy =  new  char [newLength]; 
    //拷貝數組
     System.arraycopy(original,  0,copy,  0,
                         Math.min(original.length,newLength)); 
    返回  副本; 
}複製代碼

AbstractStringBuilder的追加()方法源碼的第六行,是將字符串對象裏面字符數組裏面的內容拷貝到StringBuilder的對象的字符數組裏面,代碼以下:

str.getChars(0,len,  value,count);複製代碼

則GetChars()方法

public  void  getChars(int  srcBegin,  int  srcEnd,  char  dst [],  int  dstBegin)  {
     //中間省略了一些檢查
     ...   
    System.arraycopy( value,srcBegin,dst,dstBegin,srcEnd  -  srcBegin); 
}複製代碼

拷貝流程見下圖StringBuilder.append()執行流程

假設如今有兩個線程同時執行了StringBuilderappend()方法,兩個線程都執行完了第五行的ensureCapacityInternal()方法,此刻count=5

StringBuilder.append()執行流程2

這個時候線程1cpu時間片用完了,線程2繼續執行。線程2執行完整個append()方法後count變成6了。

StringBuilder.append()執行流程3

線程1繼續執行第六行的str.getChars()方法的時候拿到的count值就是6了,執行char[]數組拷貝的時候就會拋出ArrayIndexOutOfBoundsException異常。

至此,StringBuilder爲何不安全已經分析完了。若是咱們將測試代碼的StringBuilder對象換成StringBuffer對象會輸出什麼呢?

StringBuffer輸出結果

結果確定是會輸出 1000000,至於StringBuffer是經過什麼手段實現線程安全的呢?看下源代碼就明白了了。。。StringBuffer.append()

相關文章
相關標籤/搜索