為什麼?為什麼StringBuilder是執行緒不安全的?
之前我們對比了String、StringBuilder和StringBuffer的區別,其中一項便提到StringBuilder是非執行緒安全的,那麼是什麼原因導致了StringBuilder的執行緒不安全呢?
原因分析
如果你看了StringBuilder或StringBuffer的原始碼會說,因為StringBuilder在append操作時並未使用執行緒同步,而StringBuffer幾乎大部分方法都使用了synchronized關鍵字進行方法級別的同步處理。
上面這種說法肯定是正確的,對照一下StringBuilder和StringBuffer的部分原始碼也能夠看出來。
StringBuilder的append方法原始碼:
@Overridepublic StringBuilder append(String str) { super.append(str); return this; }
StringBuffer的append方法原始碼:
@Overridepublic synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
對於上面的結論肯定是沒什麼問題的,但並沒有解釋是什麼原因導致了StringBuilder的執行緒不安全?為什麼要使用synchronized來保證執行緒安全?如果不<typo id="typo-679" data-origin="是用" ignoretag="true">是用</typo>會出現什麼異常情況?
下面我們來逐一講解。
異常示例
我們先來跑一段程式碼示例,看看出現的結果是否與我們的預期一致。
@Testpublic 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。這種情況一旦發生便不會出現預期的結果。
拋異常的原因
回頭看異常的堆疊資訊,<typo id="typo-2537" data-origin="回" ignoretag="true">回</typo>發現有這麼一行內容:
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的執行緒不安全的原因?我們在學習和實踐的過程中,不僅要知道一些結論,還要知道這些結論的底層原理,更重要的是學會分析底層原理的方法。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70000181/viewspace-2773855/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 29-HashMap 為什麼是執行緒不安全的?HashMap執行緒
- 為什麼redis是單執行緒的以及為什麼這麼快?Redis執行緒
- 什麼是執行緒安全和執行緒不安全執行緒
- Redis為什麼是單執行緒?為什麼有如此高的效能?Redis執行緒
- redis是單執行緒的,為什麼這麼快Redis執行緒
- 為什麼 Random.Shared 是執行緒安全的random執行緒
- python為什麼要用執行緒Python執行緒
- Netty是什麼,Netty為什麼速度這麼快,執行緒模型分析Netty執行緒模型
- Redis是單執行緒的,但Redis為什麼這麼快?Redis執行緒
- redis為什麼用單執行緒不用多執行緒Redis執行緒
- 執行緒池管理(1)-為什麼需要執行緒池執行緒
- 為什麼有人說 Python 多執行緒是雞肋?Python執行緒
- 什麼是多執行緒?Python多執行緒有什麼優勢?執行緒Python
- GC 為什麼要掛起使用者執行緒? 什麼愁什麼怨?GC執行緒
- 單執行緒的 Javascript 為什麼可以非同步執行緒JavaScript非同步
- 【多執行緒與高併發】Java守護執行緒是什麼?什麼是Java的守護執行緒?執行緒Java
- Redis單執行緒,為什麼速度快Redis執行緒
- 什麼是程式(執行緒)同步執行緒
- 24. 一個普通main方法的執行,是單執行緒模式還是多執行緒模式?為什麼?AI執行緒模式
- JDK21的虛擬執行緒是什麼?和平臺執行緒什麼關係?JDK執行緒
- 什麼是Python執行緒?Python執行緒如何建立?Python執行緒
- 為什麼 Go map 和 slice 是非執行緒安全的?Go執行緒
- 什麼時候執行緒不安全?怎樣做到執行緒安全?怎麼擴充套件執行緒安全的類?執行緒套件
- Web Worker執行緒的限制是什麼?Web執行緒
- 為什麼多執行緒可以利用到多核?執行緒
- Python多執行緒是什麼意思?有什麼優勢?Python執行緒
- 為什麼使用者執行緒必須對映到核心執行緒?執行緒
- 我會手動建立執行緒,為什麼讓我使用執行緒池?執行緒
- 執行緒join為什麼在解構函式中執行緒函式
- 什麼是行為資料?企業為什麼要使用它?
- Redis作為單執行緒 為什麼我用它還是出現了超賣呢?Redis執行緒
- HashMap為何執行緒不安全HashMap執行緒
- 舉例理解什麼是程式,執行緒執行緒
- 什麼是程式、執行緒和協程?執行緒
- Netty(二) 從執行緒模型的角度看 Netty 為什麼是高效能的?Netty執行緒模型
- 【Java面試】什麼是守護執行緒,它有什麼特點Java面試執行緒
- 從執行緒池理論聊聊為什麼要看原始碼執行緒原始碼
- 為什麼dispatch_sync在主執行緒會死鎖執行緒