為什麼StringBuilder是執行緒不安全的?StringBuffer是執行緒安全的?

  • 2019 年 10 月 3 日
  • 筆記

面試中經常問到的一個問題: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,但是實際運行結果並非我們所想。

執行結果

從上圖可以看到輸出結果是9970698,並非是我們預期的1000000,並且還拋出了一個異常ArrayIndexOutOfBoundsException{非必現}

為什麼輸出結果並非預期值?

我們先看一下StringBuilder的兩個成員變數(這兩個成員變數實際上是定義在AbstractStringBuilder裡面的,StringBuilderStringBuffer都繼承了AbstractStringBuilder

AbstractStringBuilder.class

StringBuilderappend方法

StringBuilder.append(String str)

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

AbstractStringBuilder.append(String str)

我們直接看第七行程式碼,count += len; 不是一個原子操作,實際執行流程為

  • 首先載入count的值到暫存器
  • 在暫存器中執行 +1操作
  • 將結果寫入記憶體

假設我們count的值是10len的值為1,兩個執行緒同時執行到了第七行,拿到的值都是10,執行完加法運算後將結果賦值給count,所以兩個執行緒最終得到的結果都是11,而不是12,這就是最終結果小於我們預期結果的原因。

為什麼會拋出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()