這篇文章主要介紹“通過重寫hashCode()方法將偏向鎖性能提高4倍的方法步驟”,在日常操作中,相信很多人在通過重寫hashCode()方法將偏向鎖性能提高4倍的方法步驟問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”通過重寫hashCode()方法將偏向鎖性能提高4倍的方法步驟”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
福州ssl適用于網(wǎng)站、小程序/APP、API接口等需要進行數(shù)據(jù)傳輸應(yīng)用場景,ssl證書未來市場廣闊!成為成都創(chuàng)新互聯(lián)公司的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:18980820575(備注:SSL證書合作)期待與您的合作!
上周在工作中我提交了對一個類的微小改動,實現(xiàn)了toString()方法,讓日志更容易理解。令我吃驚的是,這個變動導(dǎo)致類的單元測試覆蓋率下降了5%。我知道所有的新代碼都被現(xiàn)有測試所覆蓋。那么是哪錯了呢?在比較覆蓋率報告的時候,一個眼尖的同事注意到hashCode()在變更前被測試覆蓋,而變更后卻沒有。這就說得通了:默認toString()方法調(diào)用了hashCode()方法。
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
在重寫toString()之后,自定義hashCode()不再被調(diào)用。我遺漏了一項測試。
每個人都了解toString()方法,但是……
默認hashCode()方法的所返回的值叫做標識散列碼(identity hash code)。從現(xiàn)在開始,我將使用這個術(shù)語來區(qū)分它與重寫hashCode()方法返回的散列碼。注:即使類重寫了hashCode(),你仍然可以通過System.identityHashCode(o)來獲得對象o的標識散列碼。
使用內(nèi)存地址的整型表示作為標識散列碼是常識,也是J2SE文檔所暗示的:
……通常是通過將對象的內(nèi)部地址轉(zhuǎn)換為整數(shù)來實現(xiàn)的,但這種實現(xiàn)技術(shù)并非Java編程語言所要求。
盡管如此,看起來還是有問題的,因為方法約定要求:
在Java應(yīng)用程序執(zhí)行期間,在同一對象上多次調(diào)用hashCode()方法時,hashCode()方法必須返回同一個值,無論調(diào)用的時機如何。
考慮到JVM會重新定位對象(例如在由晉升或壓縮導(dǎo)致的GC周期中)。在計算對象的標識散列碼之后,我們必須能以某種方式重新得到這個值,即使發(fā)生了對象重定位。
一種可能性是在第一次調(diào)用hashCode()時獲取對象的當(dāng)前內(nèi)存位置,然后和對象一起保存,比如保存到對象頭。這樣即使對象被移動到不同的位置,它仍然留有最初的標識散列碼。這種方法的一個隱患是:它無法阻止兩個不同對象具有相同的標識散列碼。但Java規(guī)范允許這種情況發(fā)生。
最好的確認方法是查看源代碼。不幸的是,默認的java.lang.Object::hashCode()是一個本地方法。Listing 2: Object::hashCode是本地方法
public native int hashCode();
要注意的是,標識散列碼的實現(xiàn)依賴于JVM。因為我只討論OpenJDK源代碼,所以我提到JVM時,總是指OpenJDK這一特定實現(xiàn)。代碼鏈接指向代碼倉庫的Hotspot子目錄。我認為這份代碼中的大部分也適用于Oracle JVM,當(dāng)然在個別地方可能(實際上)是不同的(稍后會詳細介紹)。
OpenJDK定義了hashCode()入口點,在源代碼src/share/vm/prims/jvm.h和src/share/vm/prims/jvm.cpp中:
508 JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle)) 509 JVMWrapper("JVM_IHashCode"); 510 // as implemented in the classic virtual machine; return 0 if object is NULL511 return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ; 512 JVM_END
identity_hash_value_for也調(diào)用了ObjectSynchronizer::FastHashCode(),前者被其他一些地方(如System.identityHashCode())調(diào)用。
708 intptr_t ObjectSynchronizer::identity_hash_value_for(Handle obj) { 709 return FastHashCode (Thread::current(), obj()) ; 710 }
有人可能簡單的認為ObjectSynchronizer::FastHashCode()的做法類似:
if (obj.hash() == 0) { obj.set_hash(generate_new_hash()); }return obj.hash();
但實際上它是包含上百行代碼、看起來復(fù)雜得多的函數(shù)。不過我們可以發(fā)現(xiàn)一些“如果沒有則生成”(if-not-exists-generate)代碼,比如:
685 mark = monitor->header(); ... 687 hash = mark->hash(); 688 if (hash == 0) { 689 hash = get_next_hash(Self, obj); ... 701 } ... 703 return hash;
這似乎證實了我們的假設(shè)?,F(xiàn)在讓我們暫時忽略管程(monitor),只要知道它可以提供對象頭。對象頭保存在變量mark中。mark是指向markOop實例的指針,markOop表示位于對象頭中低地址的標記字(mark word)。因此hashCode()的算法是:嘗試得到標記字中記錄的散列碼。如果沒有,用get_next_hash()生成一個,保存然后返回。
如我們所見,散列碼由get_next_hash()生成。這個函數(shù)提供了6種計算方法,根據(jù)全局配置hashCode選擇使用哪一個。
使用隨機數(shù)。
基于對象的內(nèi)存地址計算。
硬編碼為1(用于測試)。
從一個序列生成。
使用對象的內(nèi)存地址,轉(zhuǎn)換為int類型。
使用線程狀態(tài)和xorshift結(jié)合。
默認方法是哪一個?OpenJDK 8使用了方法5,依據(jù)是global.hpp:
1127 product(intx, hashCode, 5, \ 1128 "(Unstable) select hashCode generation algorithm") \
OpenJDK 9使用相同的默認值。查看以前的版本,OpenJDK 7和6都使用了第一個方法:隨機數(shù)。
所以,除非我找錯了源代碼,否則OpenJDK中默認hashCode()方法的實現(xiàn),和對象內(nèi)存地址無關(guān),至少從OpenJDK 6開始就是這樣。
讓我們回顧幾個之前沒有考慮的地方。首先ObjectSynchronizer::FastHashCode()似乎過于復(fù)雜,使用了超過100行代碼來執(zhí)行我們認為是平凡的“得到或生成”(get-or-generate)操作。第二,管程是什么,它為什么擁有對象頭?
查看標記詞的結(jié)構(gòu)是一個取得進展的好的起點。在OpenJDK中,它是這樣的:
30 // The markOop describes the header of an object.31 //32 // Note that the mark is not a real oop but just a word.33 // It is placed in the oop hierarchy for historical reasons.34 //35 // Bit-format of an object header (most significant first, big endian layout below):36 //37 // 32 bits:38 // --------39 // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)40 // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)41 // size:32 ------------------------------------------>| (CMS free block)42 // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)43 //44 // 64 bits:45 // --------46 // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)47 // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)48 // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)49 // size:64 ----------------------------------------------------->| (CMS free block)50 //51 // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)52 // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)53 // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)54 // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
在32位機器和64位機器上,標記字的格式略有不同。后者有兩個變體,取決于是否啟用了壓縮對象指針(Compressed Object Pointer)。Oracle JVM和OpenJDK 8都是默認啟用的。
因此對象頭可能與一個內(nèi)存塊或一個實際的對象關(guān)聯(lián),存在多種狀態(tài)。在最簡單的情況下(“普通對象”),標識散列碼直接存儲在對象頭的低地址中。
但在其他狀態(tài)下,對象頭包含一個指向JavaThread或PromotedObject的指針。更復(fù)雜的是:如果我們把唯一散列碼放到一個“普通對象”中,它會被移走嗎?移動到哪?如果對象是有偏向的(biased),我們可以從哪里獲得或設(shè)置標識散列碼?什么又是有偏向的對象(biased object)呢?
讓我們試著回答這些問題。
偏向?qū)ο罂雌饋硎瞧蜴i的結(jié)果。這是從HotSpot 6起默認啟用的一個特性,試圖減少鎖定對象的成本。鎖定操作是昂貴的,它的實現(xiàn)通常依賴于原子CPU指令(CAS),以便安全地處理來自不同線程的鎖定和解鎖請求。根據(jù)觀察,在大多數(shù)應(yīng)用程序中,大多數(shù)對象只被一個線程鎖定,因此為原子操作付出的成本常常被浪費了。為了避免這種情況,帶有偏向鎖的JVM允許線程將對象設(shè)置為“偏向于”自己。如果一個對象是有偏向的,線程可以鎖定和解鎖對象,而無需原子指令。只要沒有線程爭用同一個對象,我們就會得到性能提升。
對象頭中的偏向鎖位(biased_lock bit)表示對象是否偏向于JavaThread*所指向的線程。鎖定位(lock bit)表示該對象是否被鎖定。
正是因為OpenJDK的偏向鎖實現(xiàn)需要在標記字中寫入一個指針,它需要重新定位真正的標記字(其中包含標識散列碼)。
這可以解釋FasttHashCode中額外的復(fù)雜性。對象頭不僅包含標識散列碼,也包含鎖定狀態(tài)(比如指向鎖持有者線程的指針)。因此我們需要考慮所有情況,并找到標識散列碼存儲的位置。
讓我們來讀讀FasttHashCode。我們發(fā)現(xiàn)的第一件事是:
601 intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) { 602 if (UseBiasedLocking) { 610 if (obj->mark()->has_bias_pattern()) { ... 617 BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current()); ... 619 assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); 620 } 621 }
等等,它只是撤銷了現(xiàn)有的偏向性,并禁用了對象上的偏向鎖(false意味著不要嘗試重置偏向性)??唇酉聛淼膸仔?,這確實是一個不變量:
637 // object should remain ineligible for biased locking638 assert (!mark->has_bias_pattern(), "invariant") ;
如果我沒看錯,這意味著簡單地請求對象的標識散列碼將禁用偏向鎖,這將強制要求鎖定對象必須使用昂貴的原子指令,即使只有一個線程。
要回答這個問題,我們必須了解標記字(包含標識散列碼)可能存在的位置,這取決于對象的鎖的狀態(tài)。下面這張來自于HotSpot Wiki的圖展示了轉(zhuǎn)換過程: 我的(不可靠)推理如下。
對于圖頂部的4種狀態(tài),OpenJDK將能夠使用“輕”鎖表示。在最簡單的情況下(沒有鎖),這意味著將標識散列碼和其他數(shù)據(jù)直接放在對象的標記字中:
46 // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
在更復(fù)雜的情況下,它需要這個空間來保存指向“鎖對象”的指針。因此,標記字將被“替換”,放到其他地方。
既然只有一個線程嘗試鎖定對象,指針實際上會指向線程堆棧中的某個內(nèi)存位置。這么做有兩個優(yōu)點:訪問速度快(沒有爭用或內(nèi)存訪問協(xié)調(diào)),并且能夠讓線程確定它擁有鎖(因為內(nèi)存位置指向自己的堆棧)。
但這并非在所有情況下都有效。如果存在對象爭用(例如許多線程都會執(zhí)行到的同步語句),我們將需要一個更復(fù)雜的結(jié)構(gòu),不僅可以保存對象頭副本,也保存一組等待者。如果線程執(zhí)行object.wait(),就會出現(xiàn)對等待者列表的類似需求。
這個更豐富的數(shù)據(jù)結(jié)構(gòu)就是ObjectMonitor,在圖中稱為“重量級”管程。對象頭不再指向“被替換的標記字”,而是指向一個實際的對象(管程)。這時訪問標識散列碼需要“擴張管程(inflate the monitor)”:跟蹤指針得到對象,讀取或修改包含被替換標記字的域。這個操作更加昂貴,而且需要協(xié)調(diào)。
FasttHashCode確實有工作要做。
L640到L680處理查找對象頭并檢查緩存的標識散列碼。我相信存在一個快速路徑來探測不需要擴張管程的情況。
從L682開始需要咬緊牙關(guān):
682 // Inflate the monitor to set hash code683 monitor = ObjectSynchronizer::inflate(Self, obj); 684 // Load displaced header and check it has hash code685 mark = monitor->header(); ... 687 hash = mark->hash();
此時,如果標識散列碼存在(hash != 0),JVM可以直接返回。否則需要從get_next_hash()中得到散列碼,并安全地存儲在ObjectMonitor保存的對象頭中。
這似乎提供了一個合理的解釋,為什么在不覆蓋默認實現(xiàn)的對象上調(diào)用hashCode()導(dǎo)致對象不符合偏向鎖的條件:
為了在重定位后保持對象的標識散列碼不變,需要將標識散列碼存儲在對象頭中。
請求標識散列碼的線程未必關(guān)心對象是否鎖定,但上它們實際上共享了鎖機制使用的數(shù)據(jù)結(jié)構(gòu)。這種機制是一個復(fù)雜怪獸,它不僅自身要發(fā)生變化,還要移動(替換)對象頭。
偏向鎖能夠在不使用原子操作的情況下進行鎖定和解鎖操作。偏向鎖是高效的,如果只有一個線程鎖定對象。我們可以將鎖狀態(tài)記錄到標記字中。我不能100%肯定,但是我認為既然其他線程可能會讀取標識散列碼,即使只有一個線程需要鎖定,標記字也會發(fā)生爭用,并需要原子操作來保證準確。這否定了偏向鎖的全部意義。
默認的hashCode()實現(xiàn)(標識哈希碼)和對象的內(nèi)存地址無關(guān),至少在OpenJDK中是這樣的。在OpenJDK 6和7中,它是一個隨機生成的數(shù)字。在OpenJDK 8和9中,它是一個基于線程狀態(tài)的數(shù)字。這里有一個測試得出了相同的結(jié)論。
證明“依賴于實現(xiàn)”的警告并非虛談:Azul Zing確實從對象的內(nèi)存地址生成標識散列碼。
在HotSpot中,標識散列碼只生成一次,然后緩存在對象頭的標記字中。
Zing使用了不同的方案來保證散列碼在對象重定位后的一致的。他們在對象重定向時才保存標識散列碼的值。這個時候散列碼被保存在pre-header中。
在HotSpot中,調(diào)用默認值hashCode()或System.identityHashCode()將使對象的鎖失去偏向性。
這意味著如果你對沒有爭用的對象進行同步(synchronized),最好重寫默認的hashCode()實現(xiàn),否則將錯過JVM優(yōu)化。
在HotSpot中,可以禁用單個對象的偏向鎖。
這是非常有用的。我曾見過應(yīng)用程序在爭用的生產(chǎn)者-消費者隊列中使用過多的偏向鎖,這帶來的麻煩比好處多,所以我們完全禁用了這個特性。實際上,我們可以通過在特定對象或類上調(diào)用System.identityHashCode()來實現(xiàn)這一點。
我發(fā)現(xiàn)HotSpot沒有標志選擇默認的hashCode生成器,所以試驗其他生成器可能需要編譯源代碼。
說實話我沒仔細看。Michael Rasmussen善意地指出-XX:hashCode=2可以用來更改默認值。謝謝!
我編寫了一個簡單的JMH工具來驗證這些結(jié)論。
基準測試所做的事情類似:
object.hashCode();while(true) {synchronized(object) { counter++; } }
第一種配置(withIdHash)在使用標識散列碼的對象上同步,我們預(yù)計調(diào)用hashCode()將導(dǎo)致偏向鎖被禁用。第二種配置(withoutIdHash)實現(xiàn)了自定義散列碼,因此不會禁用偏向鎖。每個配置先用一個線程運行,然后用兩個線程(帶有后綴“Contended”)。
順便說一下,我們必須啟用-XX:BiasedLockingStartupDelay=0,否則JVM將等待4s時間才觸發(fā)優(yōu)化,這將影響測試效果。
第一次執(zhí)行:
Benchmark Mode Cnt Score Error Units BiasedLockingBenchmark.withIdHash thrpt 100 35168,021 ± 230,252 ops/ms BiasedLockingBenchmark.withoutIdHash thrpt 100 173742,468 ± 4364,491 ops/ms BiasedLockingBenchmark.withIdHashContended thrpt 100 22478,109 ± 1650,649 ops/ms BiasedLockingBenchmark.withoutIdHashContended thrpt 100 20061,973 ± 786,021 ops/ms
我們可以看到,使用自定義散列碼使鎖定和解鎖循環(huán)比使用標識散列碼(禁用偏向鎖)快4倍。當(dāng)兩個線程爭用鎖時,偏置鎖將被禁用,因此兩種散列方法之間沒有顯著差異。
第二次運行,禁用所有配置中的偏向鎖(-XX:-UseBiasedLocking)。
Benchmark Mode Cnt Score Error Units BiasedLockingBenchmark.withIdHash thrpt 100 37374,774 ± 204,795 ops/ms BiasedLockingBenchmark.withoutIdHash thrpt 100 36961,826 ± 214,083 ops/ms BiasedLockingBenchmark.withIdHashContended thrpt 100 18349,906 ± 1246,372 ops/ms BiasedLockingBenchmark.withoutIdHashContended thrpt 100 18262,290 ± 1371,588 ops/ms
散列方法不再有任何影響,withoutIdHash也失去了它的優(yōu)勢。
(所有的基準測試都運行在一臺 2.7 GHz Intel Core i5電腦上。)
這些猜想以及我對JVM源代碼的理解,來自于對關(guān)于布局、偏向鎖等不同資料的拼湊。主要的資料有:
https://blogs.oracle.com/dave/entry/biased_locking_in_hotspot
http://fuseyism.com/openjdk/cvmi/java2vm.xhtml
http://www.dcs.gla.ac.uk/~jsinger/pdfs/sicsa_openjdk/OpenJDKArchitecture.pdf
https://www.infoq.com/articles/Introduction-to-HotSpot
http://blog.takipi.com/5-things-you-didnt-know-about-synchronization-in-java-and-scala/#comment-1006598967
http://www.azulsystems.com/blog/cliff/2010-01-09-biased-locking
https://dzone.com/articles/why-should-you-care-about-equals-and-hashcode
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
https://mechanical-sympathy.blogspot.com.es/2011/11/biased-locking-osr-and-benchmarking-fun.html
package com.github.srvaroa.jmh; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 4) @Fork(value = 5, jvmArgsAppend = {"-XX:-UseBiasedLocking", "-XX:BiasedLockingStartupDelay=0"}) public class BiasedLockingBenchmark { int unsafeCounter = 0; Object withIdHash; Object withoutIdHash; @Setup public void setup() { withIdHash = new Object(); withoutIdHash = new Object() { @Override public int hashCode() { return 1; } }; withIdHash.hashCode(); withoutIdHash.hashCode(); } @Benchmark public void withIdHash(Blackhole bh) { synchronized(withIdHash) { bh.consume(unsafeCounter++); } } @Benchmark public void withoutIdHash(Blackhole bh) { synchronized(withoutIdHash) { bh.consume(unsafeCounter++); } } @Benchmark @Threads(2) public void withoutIdHashContended(Blackhole bh) { synchronized(withoutIdHash) { bh.consume(unsafeCounter++); } } @Benchmark @Threads(2) public void withIdHashContended(Blackhole bh) { synchronized(withIdHash) { bh.consume(unsafeCounter++); } } }
到此,關(guān)于“通過重寫hashCode()方法將偏向鎖性能提高4倍的方法步驟”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
當(dāng)前名稱:通過重寫hashCode()方法將偏向鎖性能提高4倍的方法步驟
瀏覽路徑:http://weahome.cn/article/pooedd.html