面試中經常問到的一個問題: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
是通過什么手段實現線程安全的呢?看下源代碼就明白了了。。。