爲什麼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
    在這裏插入圖片描述
    線程1和線程2都運行到了append方法的第五行結束,都沒有觸發擴容

    現在線程2搶佔到cpu時間片,運行第六行,將g拼接上,count=5,char數組已滿,如圖
    在這裏插入圖片描述
    那這個時候線程1再來接着運行,append方法第六行,前面分析過了str.getChars方法最終會調用一個native本地方法,System.arraycopy,可以看一下源碼
    在這裏插入圖片描述
    線程1肯定會報ArrayIndexOutofBoundsException,這就是多線程情況下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());
        }
    

    在這裏插入圖片描述

  4. 跟他相對的StringBuffer類就是一個線程安全的字符串類,可以看一下他的源代碼append方法

    	@Override
        public synchronized StringBuffer append(String str) {
            toStringCache = null;
            super.append(str);
            return this;
        }
    

    都加了synchronized鎖,所以肯定是線程安全的,但是效率肯定就是低啦。。

  5. 總結:
    詳細講解了StringBuilder的append方法實現邏輯以及多線程情況下會出現的問題
    以上都是自己個人總結,源碼分析以及圖片說明,如有問題,謝謝指正

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章