為什麼StringBuilder是執行緒不安全的?StringBuffer是執行緒安全的?
- 2019 年 10 月 3 日
- 筆記
面試中經常問到的一個問題:StringBuilder
和StringBuffer
的區別是什麼?
我們非常自信的說出:StringBuilder
是執行緒不安全的,StirngBuffer
是執行緒安全的
面試官:StringBuilder
不安全的點在哪兒?
這時候估計就啞巴了。。。
分析
StringBuffer
和StringBuilder
的實現內部是和String
內部一樣的,都是通過 char[]
數組的方式;不同的是String
的char[]
數組是通過final
關鍵字修飾的是不可變的,而StringBuffer
和StringBuilder
的char[]
數組是可變的。
首先我們看下邊這個例子:
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
裡面的,StringBuilder
和StringBuffer
都繼承了AbstractStringBuilder
)
StringBuilder
的append
方法
StringBuilder
的append
方法調用了父類的append
方法
我們直接看第七行程式碼,count += len;
不是一個原子操作,實際執行流程為
- 首先載入
count
的值到暫存器 - 在暫存器中執行
+1
操作 - 將結果寫入記憶體
假設我們count
的值是10
,len
的值為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()
方法,兩個執行緒都執行完了第五行的ensureCapacityInternal()
方法,此刻count=5
這個時候執行緒1
的cpu
時間片用完了,執行緒2
繼續執行。執行緒2執行完整個append()
方法後count
變成6
了。
執行緒1
繼續執行第六行的str.getChars()
方法的時候拿到的count
值就是6
了,執行char[]
數組拷貝的時候就會拋出ArrayIndexOutOfBoundsException
異常。
至此,StringBuilder
為什麼不安全已經分析完了。如果我們將測試程式碼的StringBuilder
對象換成StringBuffer
對象會輸出什麼呢?
結果肯定是會輸出 1000000
,至於StringBuffer
是通過什麼手段實現執行緒安全的呢?看下源程式碼就明白了了。。。