熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

Java 6中的線程優化真的有效麼?

2022-06-13   來源: Java高級技術 

  介紹 — Java 中的線程優化   

  SunIBMBEA和其他公司在各自實現的Java 虛擬機上都花費了大量的精力優化鎖的管理和同步諸如偏向鎖(biased locking)鎖粗化(lock coarsening)由逸出(escape)分析產生的鎖省略自適應自旋鎖(adaptive spinning)這些特性都是通過在應用程序線程之間更高效地共享數據從而提高並發效率盡管這些特性都是成熟且有趣的但是問題在於它們的承諾真的能實現麼?在這篇由兩部分組成的文章裡我將逐一探究這些特性並嘗試在單一線程基准的協助下回答關於性能的問題

  悲觀鎖模型

  Java支持的鎖模型絕對是悲觀鎖(其實大多數線程庫都是如此)如果有兩個或者更多線程使用數據時會彼此干擾這種極小的風險也會強迫我們采用非常嚴厲的手段防止這種情況的發生——使用鎖然而研究表明鎖很少被占用也就是說一個訪問鎖的線程很少必須等待來獲取它但是請求鎖的動作將會觸發一系列的動作這可能導致嚴重的系統開銷這是不可避免的

  

  我們的確還有其他的選擇舉例來說考慮一下線程安全的StringBuffer的用法問問你自己是否你曾經明知道它只能被一個線程安全地訪問還是堅持使用StringBuffer為什麼不用StringBuilder代替呢?

  知道大多數的鎖都不存在競爭或者很少存在競爭的事實對我們作用並不大因為即使是兩個線程訪問相同數據的概率非常低也會強迫我們使用鎖通過同步來保護被訪問的數據我們真的需要鎖麼?這個問題只有在我們將鎖放在運行時環境的上下文中觀察之後才能最終給出答案為了找到問題的答案JVM的開發者已經開始在HotSpot和JIT上進行了很多的實驗性的工作現在我們已經從這些工作中獲得了自適應自旋鎖偏向鎖和以及兩種方式的鎖消除(lock elimination)——鎖粗化和鎖省略(lock elision)在我們開始進行基准測試以前先來花些時間回顧一下這些特性這樣有助於理解它們是如何工作的

逸出分析 — 簡析鎖省略(Escape analysis lock elision explained)

  逸出分析是對運行中的應用程序中的全部引用的范圍所做的分析逸出分析是HotSpot分析工作的一個組成部分如果HotSpot(通過逸出分析)能夠判斷出指向某個對象的多個引用被限制在局部空間內並且所有這些引用都不能逸出到這個空間以外的地方那麼HotSpot會要求JIT進行一系列的運行時優化其中一種優化就是鎖省略(lock elision)如果鎖的引用限制在局部空間中說明只有創建這個鎖的線程才會訪問該鎖在這種條件下同步塊中的值永遠不會存在競爭這意味這我們永遠不可能真的需要這把鎖它可以被安全地忽略掉考慮下面的方法

    publicString concatBuffer(String s String s String s) {
StringBuffer sb = new StringBuffer();
sbappend(s);
sbappend(s);
sbappend(s);
return sbtoString();
  }

  圖 使用局部的StringBuffer連接字符串

  如果我們觀察變量sb很快就會發現它僅僅被限制在concatBuffer方法內部了進一步說到sb的所有引用永遠不會逸出到 concatBuffer方法之外即聲明它的那個方法因此其他線程無法訪問當前線程的sb副本根據我們剛介紹的知識我們知道用於保護sb的鎖可以忽略掉

  從表面上看鎖省略似乎可以允許我們不必忍受同步帶來的負擔就可以編寫線程安全的代碼了前提是在同步的確是多余的情況下鎖省略是否真的能發揮作用呢?這是我們在後面的基准測試中將要回答的問題

簡析偏向鎖(Biased locking explained)

  大多數鎖在它們的生命周期中從來不會被多於一個線程所訪問即使在極少數情況下多個線程真的共享數據了鎖也不會發生競爭為了理解偏向鎖的優勢我們首先需要回顧一下如何獲取鎖(監視器)

  獲取鎖的過程分為兩部分首先你需要獲得一份契約一旦你獲得了這份契約就可以自由地拿到鎖了為了獲得這份契約線程必須執行一個代價昂貴的原子指令釋放鎖同時就要釋放契約根據我們的觀察我們似乎需要對一些鎖的訪問進行優化比如線程執行的同步塊代碼在一個循環體中優化的方法之一就是將鎖粗化以包含整個循環這樣線程只訪問一次鎖而不必每次進入循環時都進行訪問了但是這並非一個很好的解決方案因為它可能會妨礙其他線程合法的訪問還有一個更合理的方案即將鎖偏向給執行循環的線程

  將鎖偏向於一個線程意味著該線程不需要釋放鎖的契約因此隨後獲取鎖的時候可以不那麼昂貴如果另一個線程在嘗試獲取鎖那麼循環線程只需要釋放契約就可以了Java 的HotSpot/JIT默認情況下實現了偏向鎖的優化

簡析鎖粗化(Lock coarsening explained)

  另一種線程優化方式是鎖粗化(或合並merging)當多個彼此靠近的同步塊可以合並到一起形成一個同步塊的時候就會進行鎖粗化該方法還有一種變體可以把多個同步方法合並為一個方法如果所有方法都用一個鎖對象就可以嘗試這種方法考慮圖中的實例

    public static String concatToBuffer(StringBuffer sb String s String s String s) {
sbappend(s);
sbappend(s);
sbappend(s);
return
}

  圖 使用非局部的StringBuffer連接字符串

  在這個例子中StringBuffer的作用域是非局部的可以被多個線程訪問所以逸出分析會判斷出StringBuffer的鎖不能安全地被忽略如果鎖剛好只被一個線程訪問則可以使用偏向鎖有趣的是是否進行鎖粗化與競爭鎖的線程數量是無關的在上面的例子中鎖的實例會被請求四次前三次是執行append方法最後一次是執行toString方法緊接著前一個首先要做的是將這種方法進行內聯然後我們只需執行一次獲取鎖的操作(為整個方法)而不必像以前一樣獲取四次鎖了

  這種做法帶來的真正效果是我們獲得了一個更長的臨界區它可能導致其他線程受到拖延從而降低吞吐量正因為這些原因一個處於循環內部的鎖是不會被粗化到包含整個循環體的

線程掛起 vs 自旋(Thread suspending versus spinning)

  在一個線程等待另外一個線程釋放某個鎖的時候它通常會被操作系統掛起操作在掛起一個線程的時候需要將它換出CPU而通常此時線程的時間片還沒有使用完當擁有鎖的線程離開臨界區的時候掛起的線程需要被重新喚醒然後重新被調用並交換上下文回到CPU調度中所有這些動作都會給JVMOS和硬件帶來更大的壓力

  在這個例子中如果注意到下面的事實會很有幫助鎖通常只會被占有很短的一段時間這就是說如果能夠等上一會兒我們可以避免掛起線程的開銷為了讓線程等待我們只需將線程執行一個忙循環(自旋)這項技術就是所謂的自旋鎖

  當鎖被占有的時間很短時自旋鎖的效果非常好另一方面如果鎖被占有很長時間那麼自旋的線程只會消耗CPU而不做任何有用的工作因此帶來浪費自從JDK 中引入自旋鎖以來自旋鎖被分為兩個階段自旋十個循環(默認值)然後掛起線程

自適應自旋鎖(Adaptive spinning)

  JDK 中引入了自適應自旋鎖自適應意味著自旋的時間不再固定了而是取決於一個基於前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態如果在同一個鎖對象上自旋剛剛成功過並且持有鎖的線程正在運行中那麼自旋很有可能再次成功進而它將被應用於相對更長的時間比如個循環另一方面如果自旋很少發生過它將被遺棄避免浪費任何CPU周期

StringBuffer vs StringBuilder的基准測試

  但是要想設計出一種方法來判斷這些巧妙的優化方法到底多有效這條路並不平坦首要的問題就是如何設計基准測試為了找到問題的答案我決定去看看人們通常在代碼中運用了哪些常見的技巧我首先想到的是一個非常古老的問題使用StringBuffer代替String可以減少多少開銷?

  一個類似的建議是如果你希望字符串是可變的就應該使用StringBuffer這個建議的緣由是非常明確的String是不可變的但如果我們的工作需要字符串有很多變化StringBuffer將是一個開銷較低的選擇有趣的是在遇到JDK 中的StringBuilder(它是StringBuffer的非同步版本)後這條建議就不靈了由於StringBuilder與 StringBuffer之間唯一的不同在於同步性這似乎說明測量兩者之間性能差異的基准測試必須關注在同步的開銷上我們的探索從第一個問題開始非競爭鎖的開銷如何?

  這個基准測試的關鍵(如清單所示)在於將大量的字符串拼接在一起底層緩沖的初始容量足夠大可以包含三個待連接的字符串這樣我們可以將臨界區內的工作最小化進而重點測量同步的開銷

基准測試的結果

  下圖是測試結果包括EliminateLocksUseBiasedLocking和DoEscapeAnalysis的不同組合

  

  圖 基准測試的結果

關於結果的討論

  之所以使用非同步的StringBuilder是為了提供一個測量性能的基線我也想了解一下各種優化是否真的能夠影響StringBuilder的性能正如我們所看到的StringBuilder的性能可以保持在一個不變的吞吐量水平上因為這些技術的目標在於鎖的優化因此這個結果符合預期在性能測試的另一欄中我們也可以看到使用沒有任何優化的同步的StringBuffer其運行效率比StringBuilder大概要慢三倍

  仔細觀察圖的結果我們可以注意到從左到右性能有一定的提高這可以歸功於EliminateLocks不過這些性能的提升比起偏向鎖來說又顯得有些蒼白事實上除了C列以外每次運行時如果開啟偏向鎖最終都會提供大致相同的性能提升但是C列是怎麼回事呢?

  在處理最初的數據的過程中我注意到有一項測試在六個測試中要花費格外長的時間由於結果的異常相當明顯因此基准測試似乎在報告兩個完全不同的優化行為經過一番考慮我決定同時展示出高值和低值(B列和C列)由於沒有更深入的研究我只能猜測這裡應用了一種以上的優化(很可能是兩種)並且存在一些競爭條件偏向鎖大多時候會取勝但不非總能取勝如果另一種優化占優了那麼偏向鎖的效果要麼被抑制要麼就被延遲了

  這種奇怪的現象是逸出分析導致的明確了這個基准測試的單線程化的本質後我期待著逸出分析會消除鎖從而將StringBuffer的性能提到了與 StringBuilder相同的水平但是很明顯這並沒有發生還有另外一個問題在我的機器上每一次運行的時間片分配都不盡相同更為復雜的是我的幾位同事在他們的機器上運行這些測試得到的結果更混亂了在有些時候這些優化並沒有將程序提速那麼多

前期的結論

  盡管圖列出的結果比我所期望的要少但確實可以從中看出各種優化能夠除去鎖產生的大部分開銷但是我的同事在運行這些測試時產生了不同的結果這似乎對測試結果的真實性提出了挑戰這個基准測試真的測量鎖的開銷了麼?我們的結論成熟麼?或者還有沒有其他的情況?在本文的第二部分裡我們將會深入研究這個基准測試力爭回答這些問題在這個過程中我們會發現獲取結果並不困難困難的是判斷出這些結果是否可以回答前面提出的問題

   public class LockTest { private static final int MAX = ; // million

public static void main(String[] args) throws InterruptedException { // warm up the method cache
for (int i = ; i < MAX; i++) {
     concatBuffer(Josh James Duke);
     concatBuilder(Josh James Duke);
}

Systemgc();
Threadsleep();

Systemoutprintln(Starting test);
long start = SystemcurrentTimeMillis();
for (int i = ; i < MAX; i++) {
     concatBuffer(Josh James Duke);
}
long bufferCost = SystemcurrentTimeMillis() start;
Systemoutprintln(StringBuffer: + bufferCost + ms);

Systemgc();
Threadsleep();

start = SystemcurrentTimeMillis();
for (int i = ; i < MAX; i++) {
     concatBuilder(Josh James Duke);
}
long builderCost = SystemcurrentTimeMillis() start;
Systemoutprintln(StringBuilder: + builderCost + ms);
Systemoutprintln(Thread safety overhead of StringBuffer:
+ ((bufferCost * / (builderCost * )) ) + %\n);

}

public static String concatBuffer(String s String s String s) { StringBuffer sb = new StringBuffer();
     sbappend(s);
     sbappend(s);
     sbappend(s);
     return sbtoString();
}

public static String concatBuilder(String s String s String s) {
     StringBuilder sb = new StringBuilder(); sbappend(s); sbappend(s); sbappend(s); return sbtoString();
}

}

運行基准測試

  我運行這個測試的環境是位的Windows Vista筆記本電腦配有Intel Core Duo使用Java _請注意所有的優化都是在Server VM上實現的但這在我的平台上不是默認的VM它甚至不能在JRE中使用只能在JDK中使用為了確保我使用的是Server VM我需要在命令行上打開server選項其他的選項包括


XX:+DoEscapeAnalysis off by default
XX:+UseBiasedLocking on by default
XX:+EliminateLocks on by default

  編譯源代碼運行下面的命令可以啟動測試

  javaserver XX:+DoEscapeAnalysis LockTest

關於Jeroen Borgers

  Jeroen Borger是Xebia的資深咨詢師Xebia是一家國際IT咨詢與項目組織公司專注於企業級Java和敏捷開發Jeroen幫助他的客戶攻克企業級Java系統的性能問題他同時還是Java性能調試課程的講師他在從年開始就可以在不同的Java項目中工作擔任過開發者架構師團隊lead質量負責人顧問審核員性能測試和調試員他從年開始專注於性能問題


From:http://tw.wingwit.com/Article/program/Java/gj/201311/27641.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.