淺談StringBuilder

簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處,謝謝炕檩!

連接符號 "+" 本質(zhì)

淺談Java String內(nèi)幕(1) 中斗蒋,字符串變量(非final修飾)通過 "+" 進(jìn)行拼接捌斧,在編譯過程中會轉(zhuǎn)化為StringBuilder對象的append操作,注意是編譯過程泉沾,而不是在JVM中捞蚂。

public class StringTest {
    public static void main(String[] args) {
        String str1 = "hello ";
        String str2 = "java";
        String str3 = str1 + str2 + "!";
        String str4 = new StringBuilder().append(str1).append(str2).append("!").toString();
    }
}

上述 str3 和 str4 的執(zhí)行效果其實(shí)是一樣的,不過在for循環(huán)中跷究,千萬不要使用 "+" 進(jìn)行字符串拼接姓迅。

public class test {
    public static void main(String[] args) {
        run1();
        run2();
    }   

    public static void run1() {
        long start = System.currentTimeMillis();
        String result = "";
        for (int i = 0; i < 10000; i++) {
            result += i;
        }
        System.out.println(System.currentTimeMillis() - start);
    }
    
    public static void run2() {
         long start = System.currentTimeMillis();
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            builder.append(i);
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}

在for循環(huán)中使用 "+" 和StringBuilder進(jìn)行1萬次字符串拼接,耗時(shí)情況如下:
1俊马、使用 "+" 拼接丁存,平均耗時(shí) 250ms;
2柴我、使用StringBuilder拼接解寝,平均耗時(shí) 1ms;

for循環(huán)中使用 "+" 拼接為什么這么慢艘儒?下面是run1方法的字節(jié)碼指令



5 ~ 34 行對應(yīng)for循環(huán)的代碼聋伦,可以發(fā)現(xiàn),每次循環(huán)都會重新初始化StringBuilder對象界睁,導(dǎo)致性能問題的出現(xiàn)觉增。

性能問題

StringBuilder內(nèi)部維護(hù)了一個(gè)char[]類型的value,用來保存通過append方法添加的內(nèi)容晕窑,通過 new StringBuilder() 初始化時(shí)抑片,char[]的默認(rèn)長度為16,如果append第17個(gè)字符杨赤,會發(fā)生什么敞斋?

void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

如果value的剩余容量,無法添加全部內(nèi)容疾牲,則通過expandCapacity(int minimumCapacity)方法對value進(jìn)行擴(kuò)容植捎,其中minimumCapacity = 原value長度 + append添加的內(nèi)容長度。
1阳柔、擴(kuò)大容量為原來的兩倍 + 2焰枢,為什么要 + 2,而不是剛好兩倍舌剂?
2济锄、如果擴(kuò)容之后,還是無法添加全部內(nèi)容霍转,則將 minimumCapacity 作為最終的容量大屑鼍;
3避消、利用 System.arraycopy 方法對原value數(shù)據(jù)進(jìn)行復(fù)制低滩;

在使用StringBuilder時(shí)召夹,如果給定一個(gè)合適的初始值,可以避免由于char[]數(shù)組多次復(fù)制而導(dǎo)致的性能問題恕沫。

不同初始容量的性能測試:

public class StringBuilderTest {
    public static void main(String[] args) {
        int sum = 0;
        final int capacity = 40000000;
        for (int i = 0; i < 100; i++) {
            sum += cost(capacity);
        }
        System.out.println(sum / 100);
    }

    public static long cost(int capacity) {
        long start = System.currentTimeMillis();
        StringBuilder builder = new StringBuilder(capacity);
        for (int i = 0; i < 10000000; i++) {
            builder.append("java");
        }
        return System.currentTimeMillis() - start;
    }
}

執(zhí)行一千萬次append操作监憎,不同初始容量的耗時(shí)情況如下:
1、容量為默認(rèn)16時(shí)婶溯,平均耗時(shí)110ms鲸阔;
2、容量為40000000時(shí)爬虱,不會發(fā)生復(fù)制操作隶债,平均耗時(shí)85ms腾它;

通過以上數(shù)據(jù)可以發(fā)現(xiàn)跑筝,性能損耗不是很嚴(yán)重。

內(nèi)存問題

1瞒滴、StringBuilder內(nèi)部進(jìn)行擴(kuò)容時(shí)曲梗,會新建一個(gè)大小為原來兩倍+2的char數(shù)組,并復(fù)制原char數(shù)組到新數(shù)組妓忍,導(dǎo)致內(nèi)存的消耗虏两,增加GC的壓力。
2世剖、StringBuilder的toString方法定罢,也會造成char數(shù)組的浪費(fèi)。

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

String的構(gòu)造方法中旁瘫,會新建一個(gè)大小相等的char數(shù)組祖凫,并使用 System.arraycopy() 復(fù)制StringBuilder中char數(shù)組的數(shù)據(jù),這樣StringBuilder的char數(shù)組就白白浪費(fèi)了酬凳。

重用StringBuilder

public class StringBuilderHolder {
    private final StringBuilder sb;
    public StringBuilderHolder(int capacity) {
        sb = new StringBuilder(capacity);
    }

    public StringBuilder resetAndGet() {
        sb.setLength(0);
        return sb;
    }
}

通過 sb.setLength(0) 方法可以把char數(shù)組的內(nèi)存區(qū)域設(shè)置為0惠况,這樣char數(shù)組重復(fù)使用,為了避免并發(fā)訪問宁仔,可以在ThreadLocal中使用StringBuilderHolder稠屠,使用方式如下:

private static final ThreadLocal<StringBuilderHolder> stringBuilder= new ThreadLocal<StringBuilderHolder>() {
    @Override
    protected StringBuilderHolder initialValue() {
        return new StringBuilderHolder(256);
    }
};
 
StringBuilder sb = stringBuilder.get().resetAndGet();

不過這種方式也存在一個(gè)問題,該StringBuilder實(shí)例的內(nèi)存空間一直不會被GC回收翎苫,如果char數(shù)組在某次操作中被擴(kuò)容到一個(gè)很大的值权埠,可能之后很長一段時(shí)間都不會用到如此大的空間,就會造成內(nèi)存的浪費(fèi)煎谍。

總結(jié)

雖然使用默認(rèn)的StringBuilder進(jìn)行字符串拼接操作攘蔽,性能消耗不是很嚴(yán)重,但在高性能場景下粱快,還是推薦使用ThreadLocal下可重用的StringBuilder方案秩彤。

參考資料:
StringBuilder在高性能場景下的正確用法

END叔扼。
我是占小狼。
在魔都艱苦奮斗漫雷,白天是上班族瓜富,晚上是知識服務(wù)工作者。
如果讀完覺得有收獲的話降盹,記得關(guān)注和點(diǎn)贊哦与柑。
非要打賞的話,我也是不會拒絕的蓄坏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末价捧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子涡戳,更是在濱河造成了極大的恐慌结蟋,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渔彰,死亡現(xiàn)場離奇詭異嵌屎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)恍涂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門宝惰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人再沧,你說我怎么就攤上這事尼夺。” “怎么了炒瘸?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵淤堵,是天一觀的道長。 經(jīng)常有香客問我什燕,道長粘勒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任屎即,我火速辦了婚禮庙睡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘技俐。我一直安慰自己乘陪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布雕擂。 她就那樣靜靜地躺著啡邑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪井赌。 梳的紋絲不亂的頭發(fā)上谤逼,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天贵扰,我揣著相機(jī)與錄音,去河邊找鬼流部。 笑死戚绕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枝冀。 我是一名探鬼主播舞丛,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼果漾!你這毒婦竟也來了球切?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绒障,失蹤者是張志新(化名)和其女友劉穎吨凑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體端盆,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怀骤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年费封,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了焕妙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弓摘,死狀恐怖焚鹊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情韧献,我是刑警寧澤末患,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站锤窑,受9級特大地震影響璧针,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜渊啰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一探橱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绘证,春花似錦隧膏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至魏宽,卻和暖如春腐泻,著一層夾襖步出監(jiān)牢的瞬間决乎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工派桩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瑞驱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓窄坦,卻偏偏與公主長得像唤反,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鸭津,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容