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

