Java:為什麼說StringBuilder執行緒不安全
一、前言
可能大家在學習java的基礎過程中,都知道StringBuilder相對StringBuffer效率更高,但是執行緒不安全。可是,到底是怎麼個不安全法,反正我是懵的。藉此機會,寫個小程式碼測試下。
二、編碼
既然是測試StringBuilder和StringBuffer的執行緒安全性,那就分別new一個StringBuilder和StringBuffer作為共享變數,傳遞到多個執行緒進行操作,看看最後結果如何。
package com.cjt.test; public class Test { public static void main(String[] args) { StringBuilder builder = new StringBuilder(); StringBuffer buffer = new StringBuffer(); for (int i = 0; i < 10; i++) { new A(builder, buffer).start(); } } } class A extends Thread { private StringBuilder builder; private StringBuffer buffer; A(StringBuilder builder, StringBuffer buffer) { this.buffer = buffer; this.builder = builder; } @Override public void run() { for (int i = 0; i < 100; i++) { builder.append("c"); buffer.append("c"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("[" + Thread.currentThread().getName() + "]builder:" + builder.length()); System.out.println("[" + Thread.currentThread().getName() + "]buffer:" + buffer.length()); } }
三、分析測試
我在run()
裡面加了一個Thread.sleep(10)
延時,為了更好地體現多個執行緒操作同一個變數的安全性問題。執行結果:
[Thread-9]builder:939 [Thread-5]builder:939 [Thread-9]buffer:994 [Thread-5]buffer:994 [Thread-1]builder:941 [Thread-1]buffer:996 [Thread-4]builder:941 [Thread-4]buffer:996 [Thread-0]builder:941 [Thread-0]buffer:996 [Thread-8]builder:941 [Thread-8]buffer:996 [Thread-2]builder:942 [Thread-2]buffer:998 [Thread-6]builder:942 [Thread-6]buffer:998 [Thread-3]builder:944 [Thread-3]buffer:1000 [Thread-7]builder:944 [Thread-7]buffer:1000
說實話當我執行後也是一頭問號?什麼鬼,沒有任何規律可尋。的確是這樣,但是經過多次執行後可以總結一點是:StringBuffer最終的length總是1000,但是StringBuilder的length總是少於1000。
這也太沒說服力了~!那就要分別看看它們的append()
原始碼了;
StringBuilder:
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer:
public synchronized StringBuffer append(String str) { super.append(str); return this; }
看樣子好像就隔只一個synchronized
關鍵字,那就看看super.append(str)
的原始碼吧,但可以吃驚地發現都是AbstractStringBuilder.append(String str)
,看看這段程式碼:
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
由於底層字串都是由char陣列實現的,這裡str.getChars(0, len, value, count);
就是一個明顯的賦值操作。因而可以說StringBuilder和StringBuffer的append(String str)方法都是通過getChars方法來實現字串拼接的。
有人看到上面還有個ensureCapacityInternal(count + len);
呼叫,這個只是一個底層的char陣列擴容計算,有興趣的可以自己私下看看原始碼,這裡就不貼出來。
那我們就一探getChars方法的究竟:
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
if ((srcEnd < 0) || (srcEnd > count))
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
實質上是呼叫底層System.arraycopy()
方法實現的,可能你覺得我扯遠了,其實,的確有點吧。
StringBuilder中的append方法沒有使用synchronized關鍵字,意味著多個執行緒可以同時訪問這個方法。那麼問題就來了額,如果兩個執行緒同時訪問到這個方法,那麼AbstractStringBuilder中的count是不是就是相同的,所以這兩個執行緒都是在底層char陣列的count位置開始append新增,那麼最終的結果肯定就是在後執行的那個執行緒append進去的資料會將前面一個覆蓋掉。因此我們的控制檯輸出才會出現StringBuilder一直都是小於1000的。然而StringBuffer卻不會發生這種情況。
三、總結
- StringBuilder相比StringBuffer效率更高,但多執行緒不安全;
- 在單執行緒中字串的頻繁拼接使用StringBuilder效率更高,對於多執行緒使用StringBuffer則更安全;
- 字串簡單操作時沒必要使用上述兩者,還是用String型別提高速度;