Redis源码和java jdk源码中hashcode的不同实现
一.redis實際上是使用了siphash
這個比較簡單,我說的簡單是指redis代碼比較少不像jdk一樣調用C++代碼調用棧非常深。
先看這個rehashing.c
主要就是dictKeyHash函數,需要調用dict.h頭文件中定義的dictGenHashFunction
#include "redis.h"
#include "dict.h"void _redisAssert(char *x, char *y, int l) {printf("ASSERT: %s %s %d\n",x,y,l);exit(1);
}unsigned int dictKeyHash(const void *keyp) {unsigned long key = (unsigned long)keyp;key = dictGenHashFunction(&key,sizeof(key));key += ~(key << 15);key ^= (key >> 10);key += (key << 3);key ^= (key >> 6);key += ~(key << 11);key ^= (key >> 16);return key;
}int dictKeyCompare(void *privdata, const void *key1, const void *key2) {unsigned long k1 = (unsigned long)key1;unsigned long k2 = (unsigned long)key2;return k1 == k2;
}dictType dictTypeTest = {dictKeyHash, /* hash function */NULL, /* key dup */NULL, /* val dup */dictKeyCompare, /* key compare */NULL, /* key destructor */NULL /* val destructor */
};
dict.h
uint64_t dictGenHashFunction(const void *key, int len);
uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len);
void dictEmpty(dict *d, void(callback)(void*));
void dictEnableResize(void);
void dictDisableResize(void);
int dictRehash(dict *d, int n);
int dictRehashMilliseconds(dict *d, int ms);
void dictSetHashFunctionSeed(uint8_t *seed);
uint8_t *dictGetHashFunctionSeed(void);
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, dictScanBucketFunction *bucketfn, void *privdata);
uint64_t dictGetHash(dict *d, const void *key);
dictEntry **dictFindEntryRefByPtrAndHash(dict *d, const void *oldptr, uint64_t hash);
代碼實現是在dict.c
注釋已經說明了是實現在siphash.c
/* The default hashing function uses SipHash implementation* in siphash.c. */uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k);uint64_t dictGenHashFunction(const void *key, int len) {return siphash(key,len,dict_hash_function_seed);
}uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len) {return siphash_nocase(buf,len,dict_hash_function_seed);
}
其實這個siphash.c是第三方實現的github上有源碼,這里只應用作者的說明就行了:?
/*SipHash reference C implementationCopyright (c) 2012-2016 Jean-Philippe Aumasson<jeanphilippe.aumasson@gmail.com>Copyright (c) 2012-2014 Daniel J. Bernstein <djb@cr.yp.to>Copyright (c) 2017 Salvatore Sanfilippo <antirez@gmail.com>To the extent possible under law, the author(s) have dedicated all copyrightand related and neighboring rights to this software to the public domainworldwide. This software is distributed without any warranty.You should have received a copy of the CC0 Public Domain Dedication alongwith this software. If not, see<http://creativecommons.org/publicdomain/zero/1.0/>.----------------------------------------------------------------------------This version was modified by Salvatore Sanfilippo <antirez@gmail.com>in the following ways:1. We use SipHash 1-2. This is not believed to be as strong as thesuggested 2-4 variant, but AFAIK there are not trivial attacksagainst this reduced-rounds version, and it runs at the same speedas Murmurhash2 that we used previously, why the 2-4 variant sloweddown Redis by a 4% figure more or less.2. Hard-code rounds in the hope the compiler can optimize it morein this raw from. Anyway we always want the standard 2-4 variant.3. Modify the prototype and implementation so that the function directlyreturns an uint64_t value, the hash itself, instead of receiving anoutput buffer. This also means that the output size is set to 8 bytesand the 16 bytes output code handling was removed.4. Provide a case insensitive variant to be used when hashing strings thatmust be considered identical by the hash table regardless of the case.If we don't have directly a case insensitive hash function, we need toperform a text transformation in some temporary buffer, which is costly.5. Remove debugging code.6. Modified the original test.c file to be a stand-alone function testingthe function in the new form (returing an uint64_t) using just therelevant test vector.*/
作者官網:https://131002.net/siphash/
源代碼:https://github.com/veorq/SipHash
SipHash:快速短輸入PRF
下載??|??攻擊??|??用戶??|??CRYPTANALYSIS??|??第三方實施
SipHash是一系列偽隨機函數(也稱為鍵控散列函數),針對短消息的速度進行了優化。?
目標應用程序包括網絡流量身份驗證和?防止散列泛濫?DoS攻擊。?
SipHash?安全,快速,簡單(真實):
- SipHash比以前的加密算法更簡單,更快(例如基于通用哈希的MAC)
- SipHash在性能上與不安全的?非加密算法競爭(例如MurmurHash)
我們建議哈希表切換到SipHash作為哈希函數。?SipHash的用戶已經包括FreeBSD,OpenDNS,Perl 5,Ruby或Rust。?
原始SipHash返回64位字符串。隨后根據用戶的需求創建了返回128位字符串的版本。?
知識產權:?我們不了解與SipHash相關的任何專利或專利申請,我們也不打算申請任何專利。SipHash?的參考代碼是在CC0許可下發布的,這是一種類似公共領域的許可。?
SipHash的設計者是
- Jean-Philippe Aumasson(瑞士Kudelski Security)
- Daniel J. Bernstein(美國伊利諾伊大學芝加哥分校)
聯系方式:jeanphilippe.aumasson@gmail.com???djb@cr.yp.to
下載
- 研究論文?“SipHash:快速短期投入PRF”(在DIAC研討會和INDOCRYPT 2012上?接受發表)
- 2012年INDOCRYPT(伯恩斯坦)SipHash演講的?幻燈片
- 在DIAC(Aumasson)展示SipHash的?幻燈片
- 參考C實現。
=============================================================
二.java的實現比較復雜
又要分字符串的hashCode()和object的hashCode()
1.字符串的hashCode()
/*** Returns a hash code for this string. The hash code for a* {@code String} object is computed as* <blockquote><pre>* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]* </pre></blockquote>* using {@code int} arithmetic, where {@code s[i]} is the* <i>i</i>th character of the string, {@code n} is the length of* the string, and {@code ^} indicates exponentiation.* (The hash value of the empty string is zero.)** @return a hash code value for this object.*/public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}
需要注意的是為什么 String hashCode 方法選擇數字31作為乘子,可以看看這篇帖子,這個屬于數學問題了。
原因就是
第一,31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一。另外一些相近的質數,比如37、41、43等等,也都是不錯的選擇。
第二、31可以被 JVM 優化,31 * i = (i << 5) - i。
?Stack Overflow 上關于這個問題的討論,Why does Java's hashCode() in String use 31 as a multiplier?。其中排名第一的答案引用了《Effective Java》中的一段話,這里也引用一下:
選擇值31是因為它是奇數素數。 如果它是偶數并且乘法溢出,則信息將丟失,因為乘以2相當于移位。
使用素數的優勢不太明顯,但它是傳統的。
31的一個很好的特性是乘法可以用移位和減法代替以獲得更好的性能:
31 * i ==(i << 5) - i`。 現代VM自動執行此類優化。
其他解釋:
正如 Goodrich 和 Tamassia 指出的那樣,如果你對超過 50,000 個英文單詞
(由兩個不同版本的 Unix 字典合并而成)進行 hash code 運算,
并使用常數 31, 33, 37, 39 和 41 作為乘子,每個常數算出的哈希值沖突數都小于7個,
所以在上面幾個常數中,常數 31 被 Java 實現所選用也就不足為奇了。
這個問題到底完結。
------------------------------------
2.jdk1.8 Object的hashCode()
完整的流程:
此圖出自:《hotspot中java對象默認hashcode的生成方式?》
先看hashmap算key的hashCode源碼
大量使用hash函數
翻譯如下:
/ * ----------------靜態實用程序-------------- * /
??計算key.hashCode()并將散列(XOR)更高的散列位降低。
因為該Table使用2次冪掩蔽,所以僅在當前掩碼之上的位中變化的散列組將始終發生沖突。 (在已知的例子中是一組Float鍵,在小表中保存連續的整數。)
因此,我們應用一種向下傳播較高位的影響的變換。
在速度,效用和比特擴展質量之間存在權衡。 因為許多常見的哈希集合已經合理分布(因此不會受益于傳播),并且因為我們使用樹來處理容器中的大量沖突,所以我們只是以最簡易的方式對一些移位的位進行異或,以減少系統損失, 以及由于Table邊界而包含最高位的影響,否則這些位將永遠不會用于索引計算。
直接是native的實現了
如何在jvm源碼中定位到某個Java本地方法對應的本地方法源碼?
比如說java.lang.Object#hashCode(),如何在jvm源碼定位它?
從 jdk/src/share/native/java/lang/Object.c 文件里, 你可以找到
{"hashCode", "()I", (void *)&JVM_IHashCode},{"wait", "(J)V", (void *)&JVM_MonitorWait},{"notify", "()V", (void *)&JVM_MonitorNotify},{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
大致調用鏈是:
jvm.cpp中定義了JVM_IHashCode()函數, 他又調用ObjectSynchronizer::FastHashCode;
FastHashCode在 synchronizer.cpp, FastHashCode調用get_next_hash()。
真正的計算hashcode的代碼在 synchronizer.cpp的get_next_hash()。
jvm.cpp
// java.lang.Object ///JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))JVMWrapper("JVM_IHashCode");// as implemented in the classic virtual machine; return 0 if object is NULLreturn handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
JVM_END
synchronizer.cpp?
intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {if (UseBiasedLocking) {// NOTE: many places throughout the JVM do not expect a safepoint// to be taken here, in particular most operations on perm gen// objects. However, we only ever bias Java instances and all of// the call sites of identity_hash that might revoke biases have// been checked to make sure they can handle a safepoint. The// added check of the bias pattern is to avoid useless calls to// thread-local storage.if (obj->mark()->has_bias_pattern()) {// Box and unbox the raw reference just in case we cause a STW safepoint.Handle hobj (Self, obj) ;// Relaxing assertion for bug 6320749.assert (Universe::verify_in_progress() ||!SafepointSynchronize::is_at_safepoint(),"biases should not be seen by VM thread here");BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());obj = hobj() ;assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");}}// hashCode() is a heap mutator ...// Relaxing assertion for bug 6320749.assert (Universe::verify_in_progress() ||!SafepointSynchronize::is_at_safepoint(), "invariant") ;assert (Universe::verify_in_progress() ||Self->is_Java_thread() , "invariant") ;assert (Universe::verify_in_progress() ||((JavaThread *)Self)->thread_state() != _thread_blocked, "invariant") ;ObjectMonitor* monitor = NULL;markOop temp, test;intptr_t hash;markOop mark = ReadStableMark (obj);// object should remain ineligible for biased lockingassert (!mark->has_bias_pattern(), "invariant") ;if (mark->is_neutral()) {hash = mark->hash(); // this is a normal headerif (hash) { // if it has hash, just return itreturn hash;}hash = get_next_hash(Self, obj); // allocate a new hash codetemp = mark->copy_set_hash(hash); // merge the hash code into header// use (machine word version) atomic operation to install the hashtest = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);if (test == mark) {return hash;}// If atomic operation failed, we must inflate the header// into heavy weight monitor. We could add more code here// for fast path, but it does not worth the complexity.} else if (mark->has_monitor()) {monitor = mark->monitor();temp = monitor->header();assert (temp->is_neutral(), "invariant") ;hash = temp->hash();if (hash) {return hash;}// Skip to the following code to reduce code size} else if (Self->is_lock_owned((address)mark->locker())) {temp = mark->displaced_mark_helper(); // this is a lightweight monitor ownedassert (temp->is_neutral(), "invariant") ;hash = temp->hash(); // by current thread, check if the displacedif (hash) { // header contains hash codereturn hash;}// WARNING:// The displaced header is strictly immutable.// It can NOT be changed in ANY cases. So we have// to inflate the header into heavyweight monitor// even the current thread owns the lock. The reason// is the BasicLock (stack slot) will be asynchronously// read by other threads during the inflate() function.// Any change to stack may not propagate to other threads// correctly.}// Inflate the monitor to set hash codemonitor = ObjectSynchronizer::inflate(Self, obj);// Load displaced header and check it has hash codemark = monitor->header();assert (mark->is_neutral(), "invariant") ;hash = mark->hash();if (hash == 0) {hash = get_next_hash(Self, obj);temp = mark->copy_set_hash(hash); // merge hash code into headerassert (temp->is_neutral(), "invariant") ;test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);if (test != mark) {// The only update to the header in the monitor (outside GC)// is install the hash code. If someone add new usage of// displaced header, please update this codehash = test->hash();assert (test->is_neutral(), "invariant") ;assert (hash != 0, "Trivial unexpected object/monitor header usage.");}}// We finally get the hashreturn hash;
}
// hashCode() generation :
//
// Possibilities:
// * MD5Digest of {obj,stwRandom}
// * CRC32 of {obj,stwRandom} or any linear-feedback shift register function.
// * A DES- or AES-style SBox[] mechanism
// * One of the Phi-based schemes, such as:
// 2654435761 = 2^32 * Phi (golden ratio)
// HashCodeValue = ((uintptr_t(obj) >> 3) * 2654435761) ^ GVars.stwRandom ;
// * A variation of Marsaglia's shift-xor RNG scheme.
// * (obj ^ stwRandom) is appealing, but can result
// in undesirable regularity in the hashCode values of adjacent objects
// (objects allocated back-to-back, in particular). This could potentially
// result in hashtable collisions and reduced hashtable efficiency.
// There are simple ways to "diffuse" the middle address bits over the
// generated hashCode values:
//static inline intptr_t get_next_hash(Thread * Self, oop obj) {intptr_t value = 0 ;if (hashCode == 0) {// This form uses an unguarded global Park-Miller RNG,// so it's possible for two threads to race and generate the same RNG.// On MP system we'll have lots of RW access to a global, so the// mechanism induces lots of coherency traffic.value = os::random() ;} elseif (hashCode == 1) {// This variation has the property of being stable (idempotent)// between STW operations. This can be useful in some of the 1-0// synchronization schemes.intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;} elseif (hashCode == 2) {value = 1 ; // for sensitivity testing} elseif (hashCode == 3) {value = ++GVars.hcSequence ;} elseif (hashCode == 4) {value = cast_from_oop<intptr_t>(obj) ;} else {// Marsaglia's xor-shift scheme with thread-specific state// This is probably the best overall implementation -- we'll// likely make this the default in future releases.unsigned t = Self->_hashStateX ;t ^= (t << 11) ;Self->_hashStateX = Self->_hashStateY ;Self->_hashStateY = Self->_hashStateZ ;Self->_hashStateZ = Self->_hashStateW ;unsigned v = Self->_hashStateW ;v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;Self->_hashStateW = v ;value = v ;}value &= markOopDesc::hash_mask;if (value == 0) value = 0xBAD ;assert (value != markOopDesc::no_hash, "invariant") ;TEVENT (hashCode: GENERATE) ;return value;
}
翻譯下:
hashCode()生成:
?可能性:
??* {obj,stwRandom}的MD5Digest
??* {obj,stwRandom}的CRC32或任何線性反饋移位寄存器功能。
??* DES或AES風格的SBox []機制
??*基于Phi的方案之一,例如:
????2654435761 = 2^32 * Phi (golden ratio)
????HashCodeValue =((uintptr_t(obj)>> 3)* 2654435761)^ GVars.stwRandom;
??* Marsaglia的shift-xor RNG方案的變體。
*(obj ^ stwRandom)很有吸引力,但可能導致相鄰對象(特別是背靠背分配的對象)的hashCode值出現不合需要的規律性。 這可能會導致哈希表沖突并降低哈希表效率。
有一些簡單的方法可以在生成的hashCode值上“擴散”中間地址位。
該函數提供了基于某個hashCode 變量值的六種方法。怎么生成最終值取決于hashCode這個變量值。
0 - 使用Park-Miller偽隨機數生成器(跟地址無關)
1 - 使用地址與一個隨機數做異或(地址是輸入因素的一部分)
2 - 總是返回常量1作為所有對象的identity hash code(跟地址無關)
3 - 使用全局的遞增數列(跟地址無關)
4 - 使用對象地址的“當前”地址來作為它的identity hash code(就是當前地址)
5 - 使用線程局部狀態來實現Marsaglia's xor-shift隨機數生成(跟地址無關)
Xorshift隨機數生成器是George Marsaglia發現的一類偽隨機數生成器:?
VM到底用的是哪種方法?
JDK 8 和 JDK 9 默認值:
JDK 8 以前默認值:是傳0
雖然方式不一樣,但有個共同點:java生成的hashCode和對象內存地址沒什么關系。
HotSpot提供了一個VM參數來讓用戶選擇identity hash code的生成方式:
#-XX:hashCode
參考:https://zhuanlan.zhihu.com/p/28270828
public static void main(String[] args) {int[] arr0 = new int[3];int[] arr1 = new int[3];//arr0.hashCode(); // 觸發arr0計算identity hash code//arr1.hashCode(); // 觸發arr1計算identity hash codeSystem.out.println(arr0);System.out.println(arr1);}
實驗:
交互arr0和1
兩次輸出一樣的地址,加上hashCode()就和順序有關了:
原因是:
對象的hashcode并不是在創建對象時就計算好的,而是在第一次使用的時候,也就是首次調用hashCode方法時進行計算,并存儲在對象的標記字中的。?
在VM里,Java對象會在首次真正使用到它的identity hash code(例如通過Object.hashCode() / System.identityHashCode())時調用VM里的函數來計算出值,然后會保存在對象里,后面對同一對象查詢其identity hash code時總是會返回最初記錄的值。
不是在對象創建時計算的。
這組實現代碼在HotSpot VM里自從JDK6的早期開發版開始就沒變過,只是hashCode選項的默認值變了而已。
上面的程序在執行到這個 hashCode() 調用時,VM看到對象之前還沒計算 identity hash code,才去計算并記錄它。
這樣的話,先 println(arr1) 就會使得 arr0 所引用的數組對象先被計算 identity hash code,在VM上就是從偽隨機數列中取出某一項,然后再 println(arr2) 就會計算并記錄 arr2 所引用的數組對象的 hash code,也就是取出那個偽隨機數列的下一項。反之亦然。
所以無論先 println(arr1) 還是先 println(arr2) ,看到的都是 VM用來實現 identity hash code 的偽隨機數列的某個位置的相鄰兩項,自然怎么交換都會看到一樣的結果。
而如果不調用hash code自然就會觸發identity hash code,所以交換順序就沒用...
這篇帖子也寫得很好可以看看,作者對jvm是有一些深入的研究的:《How does the default hashCode() work?》
--------------
《Java Challengers #4: Comparing Java objects with equals() and hashcode()》
《Java Challengers #2: String comparisons How String methods, keywords, and operators process comparisons in the String pool》
先看源碼
Object.java的equals:
public boolean equals(Object obj) {return (this == obj);}
String.java中的equals:
使用String類的Equals方法
該equals()方法用于驗證兩個Java類的狀態是否相同。因為equals()來自Object類,所以每個Java類都繼承它。但equals()必須重寫該方法才能使其正常工作。當然,String覆蓋equals()。
關于字符串要記住什么
Strings是不可變的,所以String不能改變狀態。- 為了節省內存,JVM將
Strings?保留在常量池中。String創建new時,JVM會檢查其值并將其指向現有對象。如果常量池中沒有該值,則JVM會創建一個新值String。 - 使用
==運算符比較對象引用。使用該equals()方法比較的值String。相同的規則將應用于所有對象。 - 使用
new運算符時,即使存在具有相同值的值,String也會在String池中創建新的運算符String。
-------------------------
下面都是Object的equals
equals()和hashcode()的常見錯誤
- 忘記
hashcode()與equals()方法一起覆蓋,反之亦然。 - 不覆蓋
equals()和hashcode()使用哈希集合時HashSet。 - 返回方法中的常量值,
hashcode()而不是返回每個對象的唯一代碼。 - 使用
==和equals互換。的==比較Object參考,而equals()比較對象值。
關于equals()和hashcode()要記住什么
- 在POJO中始終覆蓋
equals()和hashcode()方法是一種很好的做法。 - 使用有效算法生成唯一的哈希碼。
- 覆蓋
equals()方法時,也始終覆蓋該hashcode()方法。 - 該
equals()方法應該比較整個對象的狀態:來自字段的值。 - 該
hashcode()方法可以是POJO的ID。 - 當比較兩個對象的哈希碼的結果為假時,該
equals()方法也應該為假。 - 如果
equals()和hashcode()使用哈希集合時沒有被重載,集合會有重復的元素。
使用equals()和hashcode()的準則
您應該只equals()為具有相同唯一哈希碼ID的對象執行方法。當哈希碼ID?不同時,不應執行equals()。
表1.哈希碼比較
如果hashcode()比較...... | 然后 … |
|---|---|
| 返回true | 執行?equals() |
| 返回false | 不要執行?equals() |
出于性能原因,該原則主要用于Set或Hash收集。
對象比較規則
當hashcode()比較返回false,該equals()方法也必須返回false。如果哈希碼不同,則對象肯定不相等。
表2.與hashcode()的對象比較
| 當哈希碼比較返回時...... | 該equals()方法應該返回... |
|---|---|
| 真正 | 對或錯 |
| 假 | 假 |
當equals()方法返回true,這意味著該對象相等的所有的值和屬性。在這種情況下,哈希碼比較也必須為真。
表3.與equals()的對象比較
當equals()方法返回時...... | 該hashcode()方法應該返回... |
|---|---|
| 真正 | 真正 |
| 假 | 對或錯 |
總結:
==永遠是比較地址;new出來的兩個對象地址自然也是不相等的;equals默認比較地址也就是和==等效,如果是字符串是比較內容而不是地址。如果重寫了equals需要同步重寫?hashCode()
?
總結
以上是生活随笔為你收集整理的Redis源码和java jdk源码中hashcode的不同实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 泰拉瑞亚蘑菇人的房子怎么造?
- 下一篇: Java12和Jdk12安装以及Open