<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          String性能提升10倍的幾個(gè)方法!(源碼+原理分析)

          共 1784字,需瀏覽 4分鐘

           ·

          2020-08-08 11:05


          String 類型是我們使用最頻繁的數(shù)據(jù)類型,沒有之一。那么提高 String 的運(yùn)行效率,無疑是提升程序性能的最佳手段。


          我們本文將從 String 的源碼入手,一步步帶你實(shí)現(xiàn)字符串優(yōu)化的小目標(biāo)。不但教你如何有效的使用字符串,還為你揭曉這背后的深層次原因。


          本文涉及的知識(shí)點(diǎn),如下圖所示:


          在看如何優(yōu)化 String 之前,我們先來了解一下 String 的特性,畢竟知己知彼,才能百戰(zhàn)不殆。

          字符串的特性

          想要了解 String 的特性就必須從它的源碼入手,如下所示:

          //?源碼基于?JDK?1.8
          public?final?class?String
          ????implements?java.io.Serializable,?Comparable<String>,?CharSequence?
          {
          ????//?String?值的實(shí)際存儲(chǔ)容器
          ????private?final?char?value[];
          ????public?String()?{
          ????????this.value?=?"".value;
          ????}
          ????public?String(String?original)?{
          ????????this.value?=?original.value;
          ????????this.hash?=?original.hash;
          ????}
          ????//?忽略其他信息
          }

          從他的源碼我們可以看出,String 類以及它的 value[]?屬性都被 final?修飾了,其中 value[]?是實(shí)現(xiàn)字符串存儲(chǔ)的最終結(jié)構(gòu),而??final?則表示“最后的、最終的”。

          我們知道,被 final?修飾的類是不能被繼承的,也就是說此類將不能擁有子類,而被 final?修飾的變量即為常量,它的值是不能被改變的。這也就說當(dāng) String?一旦被創(chuàng)建之后,就不能被修改了。

          String 為什么不能被修改?

          String 的類和屬性 value[]?都被定義為 final?了,這樣做的好處有以下三點(diǎn):

          1. 安全性:當(dāng)你在調(diào)用其他方法時(shí),比如調(diào)用一些系統(tǒng)級(jí)操作指令之前,可能會(huì)有一系列校驗(yàn),如果是可變類的話,可能在你校驗(yàn)過后,它的內(nèi)部的值又被改變了,這樣有可能會(huì)引起嚴(yán)重的系統(tǒng)崩潰問題,所以迫使 String 設(shè)計(jì)為 final 類的一個(gè)重要原因就是出于安全考慮;
          2. 高性能:String 不可變之后就保證的 hash 值的唯一性,這樣它就更加高效,并且更適合做 HashMap 的 key- value 緩存;
          3. 節(jié)約內(nèi)存:String 的不可變性是它實(shí)現(xiàn)字符串常量池的基礎(chǔ),字符串常量池指的是字符串在創(chuàng)建時(shí),先去“常量池”查找是否有此“字符串”,如果有,則不會(huì)開辟新空間創(chuàng)作字符串,而是直接把常量池中的引用返回給此對(duì)象,這樣就能更加節(jié)省空間。例如,通常情況下 String 創(chuàng)建有兩種方式,直接賦值的方式,如 String str="Java";另一種是 new 形式的創(chuàng)建,如 String str = new String("Java")。當(dāng)代碼中使用第一種方式創(chuàng)建字符串對(duì)象時(shí),JVM 首先會(huì)檢查該對(duì)象是否在字符串常量池中,如果在,就返回該對(duì)象引用,否則新的字符串將在常量池中被創(chuàng)建。這種方式可以減少同一個(gè)值的字符串對(duì)象的重復(fù)創(chuàng)建,節(jié)約內(nèi)存。String str = new String("Java") 這種方式,首先在編譯類文件時(shí),“Java”常量字符串將會(huì)放入到常量結(jié)構(gòu)中,在類加載時(shí),“Java”將會(huì)在常量池中創(chuàng)建;其次,在調(diào)用 new 時(shí),JVM 命令將會(huì)調(diào)用 String 的構(gòu)造函數(shù),同時(shí)引用常量池中的“Java”字符串,在堆內(nèi)存中創(chuàng)建一個(gè) String 對(duì)象,最后 str 將引用 String 對(duì)象。


          不要直接+=字符串

          通過上面的內(nèi)容,我們知道了 String 類是不可變的,那么在使用 String 時(shí)就不能頻繁的 += 字符串了。

          優(yōu)化前代碼

          public?static?String?doAdd()?{
          ????String?result?=?"";
          ????for?(int?i?=?0;?i?10000;?i++)?{
          ????????result?+=?("?i:"?+?i);
          ????}
          ????return?result;
          }

          有人可能會(huì)問,我的業(yè)務(wù)需求是這樣的,那我該如何實(shí)現(xiàn)?

          官方為我們提供了兩種字符串拼加的方案:StringBuffer?和 StringBuilder,其中 StringBuilder?為非線程安全的,而 StringBuffer?則是線程安全的,StringBuffer?的拼加方法使用了關(guān)鍵字 synchronized?來保證線程的安全,源碼如下:

          @Override
          public?synchronized?StringBuffer?append(CharSequence?s)?{
          ????toStringCache?=?null;
          ????super.append(s);
          ????return?this;
          }

          也因?yàn)槭褂?synchronized?修飾,所以?StringBuffer?的拼加性能會(huì)比?StringBuilder?低。

          那我們就用?StringBuilder?來實(shí)現(xiàn)字符串的拼加,優(yōu)化后代碼

          public?static?String?doAppend()?{
          ????StringBuilder?sb?=?new?StringBuilder();
          ????for?(int?i?=?0;?i?10000;?i++)?{
          ????????sb.append("?i:"?+?i);
          ????}
          ????return?sb.toString();
          }

          我們通過代碼測(cè)試一下,兩個(gè)方法之間的性能差別:

          public?class?StringTest?{
          ????public?static?void?main(String[]?args)?{
          ????????for?(int?i?=?0;?i?5;?i++)?{
          ????????????//?String
          ????????????long?st1?=?System.currentTimeMillis();?//?開始時(shí)間
          ????????????doAdd();
          ????????????long?et1?=?System.currentTimeMillis();?//?開始時(shí)間
          ????????????System.out.println("String 拼加,執(zhí)行時(shí)間:"?+?(et1?-?st1));
          ????????????//?StringBuilder
          ????????????long?st2?=?System.currentTimeMillis();?//?開始時(shí)間
          ????????????doAppend();
          ????????????long?et2?=?System.currentTimeMillis();?//?開始時(shí)間
          ????????????System.out.println("StringBuilder 拼加,執(zhí)行時(shí)間:"?+?(et2?-?st2));
          ????????????System.out.println();
          ????????}
          ????}
          ????public?static?String?doAdd()?{
          ????????String?result?=?"";
          ????????for?(int?i?=?0;?i?10000;?i++)?{
          ????????????result?+=?("Java中文社群:"?+?i);
          ????????}
          ????????return?result;
          ????}
          ????public?static?String?doAppend()?{
          ????????StringBuilder?sb?=?new?StringBuilder();
          ????????for?(int?i?=?0;?i?10000;?i++)?{
          ????????????sb.append("Java中文社群:"?+?i);
          ????????}
          ????????return?sb.toString();
          ????}
          }

          以上程序的執(zhí)行結(jié)果如下:

          String 拼加,執(zhí)行時(shí)間:429
          StringBuilder 拼加,執(zhí)行時(shí)間:1

          String 拼加,執(zhí)行時(shí)間:325
          StringBuilder 拼加,執(zhí)行時(shí)間:1

          String 拼加,執(zhí)行時(shí)間:287
          StringBuilder 拼加,執(zhí)行時(shí)間:1

          String 拼加,執(zhí)行時(shí)間:265
          StringBuilder 拼加,執(zhí)行時(shí)間:1

          String 拼加,執(zhí)行時(shí)間:249
          StringBuilder 拼加,執(zhí)行時(shí)間:1

          從結(jié)果可以看出,優(yōu)化前后的性能相差很大。

          注意:此性能測(cè)試的結(jié)果與循環(huán)的次數(shù)有關(guān),也就是說循環(huán)的次數(shù)越多,他們性能相除的結(jié)果也越大。

          接下來,我們要思考一個(gè)問題:為什么 StringBuilder.append() 方法比 += 的性能高?而且拼接的次數(shù)越多性能的差距也越大?

          當(dāng)我們打開 StringBuilder 的源碼,就可以發(fā)現(xiàn)其中的“小秘密”了,StringBuilder 父類 AbstractStringBuilder 的實(shí)現(xiàn)源碼如下:

          abstract?class?AbstractStringBuilder?implements?Appendable,?CharSequence?{
          ????char[]?value;
          ????int?count;
          ????@Override
          ????public?AbstractStringBuilder?append(CharSequence?s,?int?start,?int?end)?{
          ????????if?(s?==?null)
          ????????????s?=?"null";
          ????????if?((start?0)?||?(start?>?end)?||?(end?>?s.length()))
          ????????????throw?new?IndexOutOfBoundsException(
          ????????????????"start?"?+?start?+?",?end?"?+?end?+?",?s.length()?"
          ????????????????+?s.length());
          ????????int?len?=?end?-?start;
          ????????ensureCapacityInternal(count?+?len);
          ????????for?(int?i?=?start,?j?=?count;?i?????????????value[j]?=?s.charAt(i);
          ????????count?+=?len;
          ????????return?this;
          ????}
          ????//?忽略其他信息...
          }

          而?StringBuilder 使用了父類提供的?char[]?作為自己值的實(shí)際存儲(chǔ)單元,每次在拼加時(shí)會(huì)修改 char[]?數(shù)組,StringBuilder toString()?源碼如下:

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

          綜合以上源碼可以看出:StringBuilder 使用了 char[]?作為實(shí)際存儲(chǔ)單元,每次在拼加時(shí)只需要修改 char[]?數(shù)組即可,只是在 toString()?時(shí)創(chuàng)建了一個(gè)字符串;而 String 一旦創(chuàng)建之后就不能被修改,因此在每次拼加時(shí),都需要重新創(chuàng)建新的字符串,所以 StringBuilder.append() 的性能就會(huì)比字符串的 += 性能高很多。

          善用 intern 方法

          善用 String.intern() 方法可以有效的節(jié)約內(nèi)存并提升字符串的運(yùn)行效率,先來看 intern()?方法的定義與源碼:

          /**
          *?Returns?a?canonical?representation?for?the?string?object.
          *?


          *?A?pool?of?strings,?initially?empty,?is?maintained?privately?by?the
          *?class?{@code?String}.
          *?


          *?When?the?intern?method?is?invoked,?if?the?pool?already?contains?a
          *?string?equal?to?this?{@code?String}?object?as?determined?by
          *?the?{@link?#equals(Object)}?method,?then?the?string?from?the?pool?is
          *?returned.?Otherwise,?this?{@code?String}?object?is?added?to?the
          *?pool?and?a?reference?to?this?{@code?String}?object?is?returned.
          *?


          *?It?follows?that?for?any?two?strings?{@code?s}?and?{@code?t},
          *?{@code?s.intern()?==?t.intern()}?is?{@code?true}
          *?if?and?only?if?{@code?s.equals(t)}?is?{@code?true}.
          *?


          *?All?literal?strings?and?string-valued?constant?expressions?are
          *?interned.?String?literals?are?defined?in?section?3.10.5?of?the
          *?The?Java??Language?Specification.
          *
          *?@return??a?string?that?has?the?same?contents?as?this?string,?but?is
          *??????????guaranteed?to?be?from?a?pool?of?unique?strings.
          */
          public?native?String?intern();

          可以看出?intern()?是一個(gè)高效的本地方法,它的定義中說的是,當(dāng)調(diào)用 intern?方法時(shí),如果字符串常量池中已經(jīng)包含此字符串,則直接返回此字符串的引用,如果不包含此字符串,先將字符串添加到常量池中,再返回此對(duì)象的引用。

          那什么情況下適合使用 intern()?方法?

          Twitter 工程師曾分享過一個(gè) String.intern() 的使用示例,Twitter 每次發(fā)布消息狀態(tài)的時(shí)候,都會(huì)產(chǎn)生一個(gè)地址信息,以當(dāng)時(shí) Twitter 用戶的規(guī)模預(yù)估,服務(wù)器需要 32G 的內(nèi)存來存儲(chǔ)地址信息。

          public?class?Location?{
          ????private?String?city;
          ????private?String?region;
          ????private?String?countryCode;
          ????private?double?longitude;
          ????private?double?latitude;
          }

          考慮到其中有很多用戶在地址信息上是有重合的,比如,國(guó)家、省份、城市等,這時(shí)就可以將這部分信息單獨(dú)列出一個(gè)類,以減少重復(fù),代碼如下:

          public?class?SharedLocation?{

          ??private?String?city;
          ??private?String?region;
          ??private?String?countryCode;
          }

          public?class?Location?{

          ??private?SharedLocation?sharedLocation;
          ??double?longitude;
          ??double?latitude;
          }

          通過優(yōu)化,數(shù)據(jù)存儲(chǔ)大小減到了 20G 左右。但對(duì)于內(nèi)存存儲(chǔ)這個(gè)數(shù)據(jù)來說,依然很大,怎么辦呢?

          Twitter 工程師使用 String.intern()?使重復(fù)性非常高的地址信息存儲(chǔ)大小從 20G 降到幾百兆,從而優(yōu)化了 String 對(duì)象的存儲(chǔ)。

          實(shí)現(xiàn)的核心代碼如下:

          SharedLocation?sharedLocation?=?new?SharedLocation();
          sharedLocation.setCity(messageInfo.getCity().intern());????
          sharedLocation.setCountryCode(messageInfo.getRegion().intern());
          sharedLocation.setRegion(messageInfo.getCountryCode().intern());

          從 JDK1.7 版本以后,常量池已經(jīng)合并到了堆中,所以不會(huì)復(fù)制字符串副本,只是會(huì)把首次遇到的字符串的引用添加到常量池中。此時(shí)只會(huì)判斷常量池中是否已經(jīng)有此字符串,如果有就返回常量池中的字符串引用。

          這就相當(dāng)于以下代碼:

          String?s1?=?new?String("Java中文社群").intern();
          String?s2?=?new?String("Java中文社群").intern();
          System.out.println(s1?==?s2);

          執(zhí)行的結(jié)果為:true

          此處如果有人問為什么不直接賦值(使用 String s1 = "Java中文社群"),是因?yàn)檫@段代碼是簡(jiǎn)化了上面 Twitter 業(yè)務(wù)代碼的語義而創(chuàng)建的,他使用的是對(duì)象的方式,而非直接賦值的方式。

          慎重使用 Split 方法

          之所以要?jiǎng)窀魑簧饔?Split?方法,是因?yàn)?Split?方法大多數(shù)情況下使用的是正則表達(dá)式,這種分割方式本身沒有什么問題,但是由于正則表達(dá)式的性能是非常不穩(wěn)定的,使用不恰當(dāng)會(huì)引起回溯問題,很可能導(dǎo)致 CPU 居高不下。

          例如以下正則表達(dá)式:

          String?badRegex?=?"^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\\\/])+$";
          String?bugUrl?=?"http://www.apigo.com/dddp-web/pdf/download?request=6e7JGxxxxx4ILd-kExxxxxxxqJ4-CHLmqVnenXC692m74H38sdfdsazxcUmfcOH2fAfY1Vw__%5EDadIfJgiEf";
          if?(bugUrl.matches(badRegex))?{
          ????System.out.println("match!!");
          }?else?{
          ????System.out.println("no?match!!");
          }
          執(zhí)行效果如下圖所示:
          可以看出,此代碼導(dǎo)致了 CPU 使用過高。

          Java 正則表達(dá)式使用的引擎實(shí)現(xiàn)是 NFA(Non deterministic Finite Automaton,不確定型有窮自動(dòng)機(jī))自動(dòng)機(jī),這種正則表達(dá)式引擎在進(jìn)行字符匹配時(shí)會(huì)發(fā)生回溯(backtracking),而一旦發(fā)生回溯,那其消耗的時(shí)間就會(huì)變得很長(zhǎng),有可能是幾分鐘,也有可能是幾個(gè)小時(shí),時(shí)間長(zhǎng)短取決于回溯的次數(shù)和復(fù)雜度。

          為了更好地解釋什么是回溯,我們使用以下面例子進(jìn)行解釋:

          text?=?"abbc";
          regex?=?"ab{1,3}c";

          上面的這個(gè)例子的目的比較簡(jiǎn)單,匹配以 a 開頭,以 c 結(jié)尾,中間有 1-3 個(gè) b 字符的字符串。

          NFA 引擎對(duì)其解析的過程是這樣子的:

          • 首先,讀取正則表達(dá)式第一個(gè)匹配符 a?和 字符串第一個(gè)字符 a?比較,匹配上了,于是讀取正則表達(dá)式第二個(gè)字符;
          • 讀取正則表達(dá)式第二個(gè)匹配符 b{1,3}?和字符串的第二個(gè)字符 b 比較,匹配上了。但因?yàn)?b{1,3}?表示 1-3 個(gè) b?字符串,以及 NFA 自動(dòng)機(jī)的貪婪特性(也就是說要盡可能多地匹配),所以此時(shí)并不會(huì)再去讀取下一個(gè)正則表達(dá)式的匹配符,而是依舊使用 b{1,3}?和字符串的第三個(gè)字符 b?比較,發(fā)現(xiàn)還是匹配上了,于是繼續(xù)使用 b{1,3}?和字符串的第四個(gè)字符 c?比較,發(fā)現(xiàn)不匹配了,此時(shí)就會(huì)發(fā)生回溯;
          • 發(fā)生回溯后,我們已經(jīng)讀取的字符串第四個(gè)字符 c?將被吐出去,指針回到第三個(gè)字符串的位置,之后程序讀取正則表達(dá)式的下一個(gè)操作符 c,然后再讀取當(dāng)前指針的下一個(gè)字符 c?進(jìn)行對(duì)比,發(fā)現(xiàn)匹配上了,于是讀取下一個(gè)操作符,然后發(fā)現(xiàn)已經(jīng)結(jié)束了。

          這就是正則匹配執(zhí)行的流程和簡(jiǎn)單的回溯執(zhí)行流程,而上面的示例在匹配到“com/dzfp-web/pdf/download?request=6e7JGm38jf.....”時(shí)因?yàn)樨澙菲ヅ涞脑?,所以程序?huì)一直讀后面的字符串進(jìn)行匹配,最后發(fā)現(xiàn)沒有點(diǎn)號(hào),于是就一個(gè)個(gè)字符回溯回去了,于是就會(huì)導(dǎo)致了 CPU 運(yùn)行過高。

          所以我們應(yīng)該慎重使用 Split() 方法,我們可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。如果實(shí)在無法滿足需求,你就在使用 Split() 方法時(shí),對(duì)回溯問題加以重視就可以了。

          總結(jié)

          本文通過 String 源碼分析,發(fā)現(xiàn)了 String 的不可變特性,以及不可變特性的 3 大優(yōu)點(diǎn)講解;然后講了字符串優(yōu)化的三個(gè)手段:不要直接 += 字符串、善用 intern() 方法和慎重使用 Split() 方法。并且通過 StringBuilder 的源碼分析,了解了 append() 性能高的主要原因,以及正則表達(dá)式不穩(wěn)定性導(dǎo)致回溯問題,進(jìn)入導(dǎo)致 CPU 使用過高的案例分析,希望可以切實(shí)的幫助到你。


          -END-



          “養(yǎng)碼場(chǎng)”
          現(xiàn)有技術(shù)人80000+
          覆蓋JAVA/PHP/IOS/測(cè)試等領(lǐng)域
          80%級(jí)別在P6及以上,含P9技術(shù)大咖30人
          技術(shù)總監(jiān)CTO?500余人

          瀏覽 53
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久1234 | 免费小视频 | 性爱一区二区三区 | 国产又黄又硬又无遮挡网站 | 大鸡吧日逼 |