天天看點

為什麼StringBuilder是線程不安全的

通常我們都知道說

StringBuilder

是線程不安全的,那如果繼續追問下去,為什麼

StringBuilder

是線程不安全的,該怎麼回答呢?

首先需要明确地知道

StringBuilder

它内部的組織結構

  1. 來看源代碼中,

    StringBuilder

    的抽象父類

    AbstractStringBuilder

    的兩個重要的成員變量
    /**
     * The value is used for character storage.
     * char數組存儲使用的字元
     */
    char[] value;
    
    /**
     * The count is the number of characters used.
     * 使用的字元的數量
     */
    int 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;
        }
               

    代碼第五行是檢查需要拼接的字元串長度跟已使用的字元長度之和是否超過char數組的長度,來決定是否擴容

    可以看一下具體的代碼實作邏輯

    private void ensureCapacityInternal(int minimumCapacity) {
            // overflow-conscious code
            if (minimumCapacity - value.length > 0) {
                value = Arrays.copyOf(value,
                        newCapacity(minimumCapacity));
            }
        }
               
    還可以繼續看一下擴容規則,

    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倍再加2,如果擴容之後的長度還是小于待拼接之後的長度,那直接使用待拼接之後的長度,後面的邏輯就是說,如果達到了最大容量的情況,這裡不做讨論了,可以繼續往下看源碼,也是不難了解的

    接着回到append方法的第六行,str.getChars方法實作了将指定字元串拼接到char數組中

    可以看一下源碼

    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);
        }
               

    前面是一系列參數校驗,最後是調用一個native本地方法,System.arraycopy實作兩個char數組從指定位置copy

    這裡為什麼是兩個char數組中?

    因為String内部也是通過一個char數組維護字元串,隻不過這個char數組成員變量是final修飾的

    繼續回到append方法,第七行就是将成員變量count(使用的字元數量)加上拼接上的字元串長度

    至此完成append方法

  2. 前面詳細分析了

    StringBuilder

    的append方法,那為什麼他就是線程不安全的呢?

    提供一個場景:

    現在char數組value的長度為5,已使用的字元數count為4

    為什麼StringBuilder是線程不安全的

    線程1和線程2都運作到了append方法的第五行結束,都沒有觸發擴容

    現線上程2搶占到cpu時間片,運作第六行,将g拼接上,count=5,char數組已滿,如圖

    為什麼StringBuilder是線程不安全的
    那這個時候線程1再來接着運作,append方法第六行,前面分析過了str.getChars方法最終會調用一個native本地方法,System.arraycopy,可以看一下源碼
    為什麼StringBuilder是線程不安全的
    線程1肯定會報

    ArrayIndexOutofBoundsException

    ,這就是多線程情況下

    StringBuilder

    不安全問題

    看到這裡應該就明白了吧,在多線程情況下,

    StringBuilder

    會出現拼接的時候發生異常導緻的不安全問題
    為什麼StringBuilder是線程不安全的
    這個異常其實不易出現,我運作了快十次才出現一次,就趕緊截圖啦。
  3. 還有一點線程不安全的情況:

    在append方法的第7行,如果兩個線程同時運作到這裡之前,拿到的count相同的,然後繼續往下執行,這樣的話到最後count的值必定會少算,這個通過測試結果也很好出現。

    public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 100; j++) {
                        str.append("a");
                    }
                }).start();
            }
            Thread.sleep(2000);
            System.out.println(str.length());
        }
               
    為什麼StringBuilder是線程不安全的
  4. 跟他相對的

    StringBuffer

    類就是一個線程安全的字元串類,可以看一下他的源代碼append方法
    @Override
        public synchronized StringBuffer append(String str) {
            toStringCache = null;
            super.append(str);
            return this;
        }
               
    都加了synchronized鎖,是以肯定是線程安全的,但是效率肯定就是低啦。。
  5. 總結:

    詳細講解了

    StringBuilder

    的append方法實作邏輯以及多線程情況下會出現的問題

    以上都是自己個人總結,源碼分析以及圖檔說明,如有問題,謝謝指正