三個常見的程式碼效能優化方式
編寫有效率的程式碼是我們的一項基本技能。我們千萬不要忽視程式碼的效能要求。越早考慮效能問題,需要支付的成本就越小,帶來的價值就越大,不要等到出現效能問題時,才去臨時抱佛腳。如果前期沒有看重程式碼的效能問題,那麼後期我們就要付出加倍的精力去維護和重構程式碼。
程式碼的效能並不是可以多塊地進行加減乘除,而是如何管理記憶體、磁碟、網路、核心等計算機資源,已達到效能最優化。
在這篇文章裡,我選了三個常見且實用的程式碼效能優化方式,供你參考和借鑑。
讓介面保持簡單直觀的兩個小技巧
設計介面之所以難,在於介面對穩定性的要求比較高。要想保證介面的穩定性,最有效的方法就是讓介面設計得簡單直觀一些。在工作中,我總結了兩個小技巧供你參考使用。
學會拆解問題
設計軟體介面,要從實際的問題出發,只有這樣,我們才能找到一條清晰的主線。圍繞這條主線展開設計,就可以有效地避免需求膨脹和過渡設計。
拆解問題,需要遵循兩個原則—— 相互獨立,完全窮盡。
比如說,是否可以授權一個使用者使用某一個線上服務呢?這個問題就可以分解為兩個小問題:
1、該使用者是否為已註冊的使用者?
2、該使用者是否持有正確的密碼?
我們可以使用思維導圖來描述這個分解。
這種劃分其實是有問題的。因為只有已經註冊的使用者,才會持有正確的密碼。而且,只有持有正確密碼的使用者,才能夠被看作是註冊使用者。這兩個小問題之間,存在著依賴關係,就不能算是“相互獨立”。
我們要消除掉這種依賴關係,這樣需要兩個層次的表達。第一個層次問題是,該使用者是否為已註冊的使用者?這個問題,可以進一步分解為兩個更小的問題:使用者持有的使用者名稱是否已註冊? 使用者持有的密碼是否匹配?
1、該使用者是否是已註冊的使用者?
使用者名稱是否已註冊?
使用者密碼是否正確?
但這樣還是有缺陷的。如果一個服務,對所有的註冊使用者開放,上面的分解就是完備的。否則,我們就漏掉了一個重要的內容,不同的註冊使用者,可以訪問的服務可能是不同的。也就是說如果沒有訪問的許可權,那麼即使使用者名稱和密碼正確也無法訪問相關的服務。
如果我們把漏掉的加上,這個問題的分解可以進一步表示為:
1、該使用者是否是已註冊的使用者?
使用者名稱是否已註冊?
使用者密碼是否正確?
2、該使用者是否有訪問的許可權?
到這一步,我們就會有一個清晰的思路了。
一個介面解決一件事情
這裡所說的“事情”,其實是在某一個層級上的一個職責,比如授權使用者訪問是一件完整、獨立的事情。有了邏輯級別,我們才能分解問題,介面之間才能建立聯絡。
對於一件事的劃分,我們要注意三點。
1、一件事就是一件事,不是兩件事,也不是三件事。
2、這件事是獨立的。
3、這件事是完整的。
我們以一段程式碼為例,看一下如果介面不明確會產生怎樣的後果。
複製程式碼
/** * A {@code HelloWords} objectisresponsiblefordetermining how to say *"Hello"indifferent language. */ classHelloWords{ privateStringlanguage= "English"; privateStringgreeting= "Hello"; {1} //snipped {1} /** *Setthelanguageofthegreeting. * * @paramlanguagethelanguageofthegreeting. */ voidsetLanguage(String language){ //snipped } {1} /** *Setthegreetingsofthegreeting. * * @paramlanguagethegreetingsofthegreeting. */ voidsetGreeting(String greeting){ //snipped } {1} //snipped } {1}
這段程式碼涉及兩個要素,一個是語言(英語、漢語等),一個是問候語(Hello、你好等),它抽象出了這兩個要素。使用 setLanguage() 設定問候的語言,使用 setGreeting() 設定問候的問候語。但這樣的設計對使用者是不友好的。因為 setLanguage() 和 setGreeting() 這兩個方法,都不能表達一個完整的事情。只有兩個方法合起來,才能表達一件完整的事情。
這種互相依賴的關係,會導致很多問題。 比如說:
1、使用時,應該先呼叫哪一個方法?
2、如果語言和問候語不匹配,會出現什麼情況?
3、實現時,需不需要匹配語言和問候語?
4、實現時,該怎麼匹配語言和問候語?
所以我們應當牢記,介面應該儘可能只解決一件事情,如果實在做不到,就需要減少依賴關係。
想了解更多有關介面設計的內容,請點選: 怎麼設計一個簡單又直觀的介面?
學會使用 JMH,避免效能陷阱
我們如何才能知道自己編寫的程式碼的效能呢?事實上,Java 提供了一個性能測試工具 JMH,它可以直觀地幫助我們檢視程式碼的效能缺陷和陷阱。
JMH 的使用方法
首先,使用 Maven 工具建立一個基準測試專案:
複製程式碼
mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeGroupId=org.openjdk.jmh \ -DarchetypeArtifactId=jmh-java-benchmark-archetype \ -DgroupId=com.example \ -DartifactId=myJmh \ -Dversion=1.0
然後編譯基準測試:
複製程式碼
cd myJmh $ mvn clean install
最後執行編譯測試:
複製程式碼
cd myJmh $ Java -jar target/benchmarks.jar
下面是執行結果,我們需要注意到 Score 這一欄,它顯示的是每秒可以執行的基準測試方法的次數。次數越多,效率越高。
複製程式碼
Benchmark Mode Cnt Score Error Units MyBenchmark.testMethod thrpt2535.945▒0.694ops/s
下面我們通過執行三個字串 String、StringBuilder 和 StringBuffer,來看下這三個字串的效能差異。為了方便對比,JMH 的測試結果,都寫在了註釋裡。
複製程式碼
// JMH throughput benchmark: about 32 operations per second @Benchmark publicStringmeasureStringApend(){ String targetString =""; for(inti =0; i <10000; i++) { targetString +="hello"; } returntargetString; } // JMH throughput benchmark: about 5,600 operations per second @Benchmark publicStringmeasureStringBufferApend(){ StringBuffer buffer =newStringBuffer(); for(inti =0; i <10000; i++) { buffer.append("hello"); } returnbuffer.toString(); } // JMH throughput benchmark: about 21,000 operations per second @Benchmark publicStringmeasureStringBuilderApend(){ StringBuilder builder =newStringBuilder(); for(inti =0; i <10000; i++) { builder.append("hello"); } returnbuilder.toString(); }
你可能會看到,使用 String 的效能是最差的,StringBuilder 的字串連線操作,比使用 String 的操作快了近 200 倍,而 StringBuffer 的字串連線操作,更是快了近 700 倍。
為什麼 String 的效率如此慢?這是因為每一個字串連線的操作,都需要建立一個新的 String 物件,然後再銷燬,再建立。這種模式對 CPU 和記憶體消耗都比較大。
StringBuilder 為什麼比 StringBuffer 還要快呢?StringBuffer 的字串操作是多執行緒安全的,而 StringBuilder 的操作就不是。如果我們看這兩個方法的實現程式碼,除了執行緒安全的同步以外,幾乎沒有差別。
通過上面的基準測試,我們可以得出這樣的結論:
1、頻繁的物件建立、銷燬,有損程式碼的效率;
2、減少記憶體分配、拷貝、釋放的頻率,可以提高程式碼的效率;
3、即使是單執行緒環境,使用執行緒同步依然有損程式碼的效率。
但這並不意味著使用 StringBuilder 會更好,想要檢視更多基準測試和結論,請點選: 有哪些招惹麻煩的效能陷阱?
超越執行緒同步的技巧
我們都知道,執行緒同步有損效率。在實際工作中,我們只要打破下面的任何一個條件,就不需要使用執行緒同步了:
使用單執行緒;
1、不關心共享資源的變化;
2、沒有改變共享資源的行為。
3、應用到具體的工作場景中,又該怎麼避免執行緒同步呢?
學會使用 final 關鍵字
Java 裡面的 final 關鍵字,可以把變數改為不可變的量。在軟體環境裡,不可變,就意味著一旦例項化,就不再改變。
比如下面這段程式碼是沒有使用 final 關鍵字的。如果只有一個執行緒,這段程式碼就沒有問題。但是,如果有兩個執行緒,一個執行緒讀,一個執行緒寫,就會出現競爭狀況,返回不匹配的語言環境和問候語。
複製程式碼
classHelloWords { privateStringlanguage ="English"; privateStringgreeting ="Hello"; voidsetLanguage(Stringlanguage) { this.language = language; } voidsetGreeting(Stringgreeting) { this.greeting = greeting; } StringgetLanguage() { returnlanguage; } StringgetGreeting() { returngreeting ; } }
如果我們使用了 final 關鍵字,類變數只能被賦值一次,而且只能在例項化之前被賦值。這樣的變數,就是不可變的量。如果一個類的所有的變數,都是不可變的,那麼這個類也是不可變的。
複製程式碼
classHelloWords{ privatefinalStringlanguage; privatefinalStringgreeting; HelloWords(Stringlanguage,Stringgreeting) { this.language = language; this.greeting = greeting; } StringgetLanguage() { returnlanguage; } StringgetGreeting() { returngreeting ; } }
所以,我們要養成一個習慣,看到宣告的變數,就要琢磨,這個變數能不能宣告成不可變的量?有沒有辦法修改介面設計或者實現程式碼,把它改成不可變的量?設計一個類時,要優先考慮,這個類是不是可以設計成不可變的類?這樣就可以避免很多不必要的執行緒同步,讓程式碼的效率更高,介面更容易使用。
更多有關超越執行緒同步的內容,請點選: 高效率,從超越執行緒同步開始! 進行留言互動。
你在工作中還會遇到哪些有關程式碼的效能問題?又是如何解決的呢?歡迎你在評論區與我分享你的經驗和心得。
文章選自 《程式碼精進之路》