1. 程式人生 > >Java:為什麼說StringBuilder執行緒不安全

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卻不會發生這種情況。

三、總結

  1. StringBuilder相比StringBuffer效率更高,但多執行緒不安全;
  2. 在單執行緒中字串的頻繁拼接使用StringBuilder效率更高,對於多執行緒使用StringBuffer則更安全;
  3. 字串簡單操作時沒必要使用上述兩者,還是用String型別提高速度;