為什么StringBuilder是線程不安全的?
在前面的面試題講解中我們對比了String、StringBuilder和StringBuffer的區(qū)別,其中一項便提到StringBuilder是非線程安全的,那么是什么原因?qū)е铝薙tringBuilder的線程不安全呢?
原因分析
如果你看了StringBuilder或StringBuffer的源代碼會說,因為StringBuilder在append操作時并未使用線程同步,而StringBuffer幾乎大部分方法都使用了synchronized關(guān)鍵字進行方法級別的同步處理。
上面這種說法肯定是正確的,對照一下StringBuilder和StringBuffer的部分源代碼也能夠看出來。
StringBuilder的append方法源代碼:
public StringBuilder append(String str) {super.append(str);return this;}
StringBuffer的append方法源代碼:
public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}
對于上面的結(jié)論肯定是沒什么問題的,但并沒有解釋是什么原因?qū)е铝薙tringBuilder的線程不安全?為什么要使用synchronized來保證線程安全?如果不是用會出現(xiàn)什么異常情況?
下面我們來逐一講解。
異常示例
我們先來跑一段代碼示例,看看出現(xiàn)的結(jié)果是否與我們的預期一致。
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();}// 睡眠確保所有線程都執(zhí)行完Thread.sleep(1000);System.out.println(sb.length());}
上述業(yè)務邏輯比較簡單,就是構(gòu)建一個StringBuilder,然后創(chuàng)建10個線程,每個線程中拼接字符串“a”1000次,理論上當線程執(zhí)行完成之后,打印的結(jié)果應該是10000才對。
但多次執(zhí)行上面的代碼打印的結(jié)果是10000的概率反而非常小,大多數(shù)情況都要少于10000。同時,還有一定的概率出現(xiàn)下面的異常信息“
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsExceptionat 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數(shù)組value和count。StringBuilder通過對value的不斷擴容和count對應的增加來完成字符串的append操作。
// 存儲的字符串(通常情況一部分為字符串內(nèi)容,一部分為默認值)char[] value;// 數(shù)組已經(jīng)使用數(shù)量int count;
上面的這兩個屬性均位于它的抽象父類AbstractStringBuilder中。
如果查看構(gòu)造方法我們會發(fā)現(xiàn),在創(chuàng)建StringBuilder時會設置數(shù)組value的初始化長度。
public StringBuilder(String str) {super(str.length() + 16);append(str);}
默認是傳入字符串長度加16。這就是count存在的意義,因為數(shù)組中的一部分內(nèi)容為默認值。
當調(diào)用append方法時會對count進行增加,增加值便是append的字符串的長度,具體實現(xiàn)也在抽象父類中。
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;}
我們所說的線程不安全的發(fā)生點便是在append方法中count的“+=”操作。我們知道該操作是線程不安全的,那么便會發(fā)生兩個線程同時讀取到count值為5,執(zhí)行加1操作之后,都變成6,而不是預期的7。這種情況一旦發(fā)生便不會出現(xiàn)預期的結(jié)果。
拋異常的原因
回頭看異常的堆棧信息,回發(fā)現(xiàn)有這么一行內(nèi)容:
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底層發(fā)生的。arraycopy的核心操作就是將傳入的String對象copy到value當中。
而異常發(fā)生的原因是明明value的下標只到6,程序卻要訪問和操作下標為7的位置,當然就跑異常了。
那么,為什么會超出這么一個位置呢?這與我們上面講到到的count被少加有關(guān)。在執(zhí)行str.getChars方法之前還需要根據(jù)count校驗一下當前的value是否使用完畢,如果使用完了,那么就進行擴容。append中對應的方法如下:
ensureCapacityInternal(count + len);ensureCapacityInternal的具體實現(xiàn):
private void ensureCapacityInternal(int minimumCapacity) {// overflow-conscious codeif (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value,newCapacity(minimumCapacity));}}
count本應該為7,value長度為6,本應該觸發(fā)擴容。但因為并發(fā)導致count為6,假設len為1,則傳遞的minimumCapacity為7,并不會進行擴容操作。這就導致后面執(zhí)行str.getChars方法進行復制操作時訪問了不存在的位置,因此拋出異常。
這里我們順便看一下擴容方法中的newCapacity方法:
private int newCapacity(int minCapacity) {// overflow-conscious codeint newCapacity = (value.length << 1) + 2;if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)? hugeCapacity(minCapacity): newCapacity;}
除了校驗部分,最核心的就是將新數(shù)組的長度擴充為原來的兩倍再加2。把計算所得的新長度作為Arrays.copyOf的參數(shù)進行擴容。
小結(jié)
經(jīng)過上面的分析,是不是真正了解了StringBuilder的線程不安全的原因?我們在學習和實踐的過程中,不僅要知道一些結(jié)論,還要知道這些結(jié)論的底層原理,更重要的是學會分析底層原理的方法。
