在前面的面試題講解中我們對比了String、StringBuilder和StringBuffer的區別,其中一項便提到StringBuilder是非執行緒安全的,那么是什么原因導致了StringBuilder的執行緒不安全呢?
原因分析
如果你看了StringBuilder或StringBuffer的源代碼會說,因為StringBuilder在append操作時并未使用執行緒同步,而StringBuffer幾乎大部分方法都使用了synchronized關鍵字進行方法級別的同步處理,
上面這種說法肯定是正確的,對照一下StringBuilder和StringBuffer的部分源代碼也能夠看出來,
StringBuilder的append方法源代碼:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer的append方法源代碼:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
對于上面的結論肯定是沒什么問題的,但并沒有解釋是什么原因導致了StringBuilder的執行緒不安全?為什么要使用synchronized來保證執行緒安全?如果不是用會出現什么例外情況?
下面我們來逐一講解,
例外示例
我們先來跑一段代碼示例,看看出現的結果是否與我們的預期一致,
@Test
public void test() throws InterruptedException {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
sb.append("a");
}
}).start();
}
// 睡眠確保所有執行緒都執行完
Thread.sleep(1000);
System.out.println(sb.length());
}
上述業務邏輯比較簡單,就是構建一個StringBuilder,然后創建10個執行緒,每個執行緒中拼接字串“a”1000次,理論上當執行緒執行完成之后,列印的結果應該是10000才對,
但多次執行上面的代碼列印的結果是10000的概率反而非常小,大多數情況都要少于10000,同時,還有一定的概率出現下面的例外資訊“
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException
at java.lang.System.arraycopy(Native Method)
at java.lang.String.getChars(String.java:826)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.secbro2.strings.StringBuilderTest.lambda$test$0(StringBuilderTest.java:18)
at java.lang.Thread.run(Thread.java:748)
9007
執行緒不安全的原因
StringBuilder中針對字串的處理主要依賴兩個成員變數char陣列value和count,StringBuilder通過對value的不斷擴容和count對應的增加來完成字串的append操作,
// 存盤的字串(通常情況一部分為字串內容,一部分為默認值)
char[] value;
// 陣列已經使用數量
int count;
上面的這兩個屬性均位于它的抽象父類AbstractStringBuilder中,
如果查看構造方法我們會發現,在創建StringBuilder時會設定陣列value的初始化長度,
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
默認是傳入字串長度加16,這就是count存在的意義,因為陣列中的一部分內容為默認值,
當呼叫append方法時會對count進行增加,增加值便是append的字串的長度,具體實作也在抽象父類中,
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
我們所說的執行緒不安全的發生點便是在append方法中count的“+=”操作,我們知道該操作是執行緒不安全的,那么便會發生兩個執行緒同時讀取到count值為5,執行加1操作之后,都變成6,而不是預期的7,這種情況一旦發生便不會出現預期的結果,
拋例外的原因
回頭看例外的堆疊資訊,回發現有這么一行內容:
at java.lang.String.getChars(String.java:826)
對應的代碼就是上面AbstractStringBuilder中append方法中的代碼,對應方法的源代碼如下:
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
其實體外是最后一行arraycopy時JVM底層發生的,arraycopy的核心操作就是將傳入的String物件copy到value當中,
而例外發生的原因是明明value的下標只到6,程式卻要訪問和操作下標為7的位置,當然就跑例外了,
那么,為什么會超出這么一個位置呢?這與我們上面講到到的count被少加有關,在執行str.getChars方法之前還需要根據count校驗一下當前的value是否使用完畢,如果使用完了,那么就進行擴容,append中對應的方法如下:
ensureCapacityInternal(count + len);
ensureCapacityInternal的具體實作:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
count本應該為7,value長度為6,本應該觸發擴容,但因為并發導致count為6,假設len為1,則傳遞的minimumCapacity為7,并不會進行擴容操作,這就導致后面執行str.getChars方法進行復制操作時訪問了不存在的位置,因此拋出例外,
這里我們順便看一下擴容方法中的newCapacity方法:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
除了校驗部分,最核心的就是將新陣列的長度擴充為原來的兩倍再加2,把計算所得的新長度作為Arrays.copyOf的引數進行擴容,
小結
經過上面的分析,是不是真正了解了StringBuilder的執行緒不安全的原因?我們在學習和實踐的程序中,不僅要知道一些結論,還要知道這些結論的底層原理,更重要的是學會分析底層原理的方法,
原文鏈接:《為什么StringBuilder是執行緒不安全的?》

CSDN認證博客專家
Spring
架構
Java
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/108573.html
標籤:其他
