`
NanguoCoffee
  • 浏览: 51211 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

知道为啥HashMap里面的数组size必须是2的次幂?

阅读更多

最近在写一个简易的分离锁的类:

 

要求:对不同的Key进行hash得到一个Lock,并要求对锁映射的概率差不多。比如,160个Key,分布到16个锁上,大概有10个Key是映射到同一个锁上的,只要这样并发效率才会高。

 

public class SplitReentrantLock {

	private Lock[] locks;

	private int LOCK_NUM;

	public SplitReentrantLock(int lockNum) {
		super();
		LOCK_NUM = lockNum;
		locks = new Lock[LOCK_NUM];
		for (int i = 0; i < LOCK_NUM; i++) {
			locks[i] = new ReentrantLock();
		}
	}

	/**
	 * 获取锁, 使用HashMap的hash算法
	 * 
	 * 
	 * @param key
	 * @return
	 */
	public Lock getLock(String key) {

		int lockIndex = index(key);
		return locks[lockIndex];
	}

	int index(String key) {
		int hash = hash(key.hashCode());		
		return hash & (LOCK_NUM - 1);
	}

	int hash(int h) {
		h ^= (h >>> 20) ^ (h >>> 12);
		return h ^ (h >>> 7) ^ (h >>> 4);
	}

 

 

用法:

 

SplitReentrantLock locks = new SplitReentrantLock(16);
  Lock lock =locks.getLock(key); 
  lock.lock();
  try{
     //......
   }finally{
   lock.unlock(); 
   }

 

本来认为用HashMap的hash算法就能够将 达到上述的要求,结果测试的时候吓了一跳。

 

测试代码:

 

 

public class SplitReenterLockTest extends TestCase {

	public void method(int lockNum, int testNum) {

		SplitReentrantLock splitLock = new SplitReentrantLock(lockNum);
		Map<Integer, Integer> map = new TreeMap<Integer, Integer>();
		for (int i = 0; i < lockNum; i++) {
			map.put(i, 0);
		}
		for (int i = 0; i < testNum; i++) {
			Integer key = splitLock.index(RandomStringUtils.random(128));
			map.put(key, map.get(key) + 1);
		}

		for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
			System.out.println(entry.getKey() + " : " + entry.getValue());
		}
	}

	public void test1() {
		method(50, 1000);}
 
}

 

 

结果:1000个随机key的hash只是映射到8个 Lock上,而不是平均到50个Lock上。

而且是固定分布到0,1,16,17,32,33,48,49的数组下标对应的Lock上面,这是为什么呢?

 

如果改为:

 

public void test1() {
	method(32, 1000);
}

 

 结果:1000个随机key的hash 映射到32个Lock上,而且基本上是平均分布的。

 

问题 :为什么50和32的hash的效果差别那么大呢?

 

再次测试2,4,8,16,64,128. 发现基本上都是平均分布到所有的Lock上面。

 

得到平均分布的这些数都是2的次幂,难道hash算法和二进制有关?

 

看看hash算法:   

 

   int index(String key) {
		int hash = hash(key.hashCode());		
		return hash & (LOCK_NUM - 1);
	}

	int hash(int h) {
		h ^= (h >>> 20) ^ (h >>> 12);
		return h ^ (h >>> 7) ^ (h >>> 4);
	}

 先是经过神奇的(ps:不知道为什么这么运算,无知的我只能用神奇来形容)的位运算,最后和LOCK_NUM - 1来进行与运算。

 

本帖的关键点就是在于这个与运算中,如果要想运算后的结果是否平均分布,在于LOCK_NUM-1的二进制中1的位数有几个。如果都是1,那么肯定是平均分布到0至LOCK_NUM-1上面。否则仅仅分布指定的几位。

 

下面以50和32说明:

 

假设Key进行hash运行得到hash值为h,

比如:我测试的数据中的一些h的二进制值:

 

1100000010000110110101010001001
10111100001001110111000100010001
11111011111010101010000111001001
11001010011000100110110111011111
10001010100010111101011010011110

 50的二进制值:110010.减去1后的二进制:110001

 32的二进制值:  100000.减去1后的二进制:11111

 

因此h和 49 (即110001)与的结果只能为

000000  : 0

000001  : 1

010000  : 16

010001  : 17

100000  : 32

100001  : 33

110000  : 48

110001  : 49

 

而h和31 (即11111)与的结果为:

00000

00001

00010

....

11110

11111

 

这下知道原因了吧。LOCK_NUM -1 二进制中为1的位数越多,那么分布就平均。

 

 

这也就是为什么HashMap默认大小为2的次幂,并且添加元素时,如果超过了一定的数量,那么就将数量增大到原来的两倍,其中非常重要的原因就是为了hash的平均分布

 

分享到:
评论
13 楼 NanguoCoffee 2011-02-18  
javantsky 写道
楼主为什么要自己实现分离锁呢?

java.util.concurrent.ConcurrentHashMap<K,V>
这个已经帮你搞定了



分离锁和Map没什么必然的关系呀。
分离锁的应用场景和Map的应用场景不同呀。

要求这样:
SplitReentrantLock locks = new SplitReentrantLock(16);
  Lock lock =locks.getLock(key); 
  lock.lock();
  try{
     //......
   }finally{
   lock.unlock(); 
   }





12 楼 javantsky 2011-02-18  
楼主为什么要自己实现分离锁呢?

java.util.concurrent.ConcurrentHashMap<K,V>
这个已经帮你搞定了
11 楼 obullxl 2011-02-18  
LZ分析有道理,最后的&操作,(LOCK_NUM - 1)的二进制1越多,越是平均分布,反过来说,也就是2^n会平均分布。
10 楼 NanguoCoffee 2011-02-17  
sniffer123 写道
LZ你自己的写法有问题啊。。跟HASH是不是 2的幂一点关系也没有
hash & (LOCK_NUM - 1) 能等同 hash % (LOCK_NUM - 1)吗?
假如LOCK_NUM = 14,那么 (LOCK_NUM - 1) 就是 二进制 1101
你用&,第二位永远都是会被去掉的,简单点说,如果hash 是 1~100,&后出来的结果分布肯定是不平均的
之所以会出现你那个只要是2^n就会分布平均,是因为这个时候,2^n的二进制是 1111,&出来相当于取低四位。


恩,在SplitReentrantLock 中使用hash & (LOCK_NUM - 1)确实达不到平均分布的要求。
直接拷贝hashMap.index(...),刚开始没注意,测试过后才发现的。

我没说和hash值有关。
只是和数组的长度有关。

9 楼 sniffer123 2011-02-17  
LZ你自己的写法有问题啊。。跟HASH是不是 2的幂一点关系也没有
hash & (LOCK_NUM - 1) 能等同 hash % (LOCK_NUM - 1)吗?
假如LOCK_NUM = 14,那么 (LOCK_NUM - 1) 就是 二进制 1101
你用&,第二位永远都是会被去掉的,简单点说,如果hash 是 1~100,&后出来的结果分布肯定是不平均的
之所以会出现你那个只要是2^n就会分布平均,是因为这个时候,2^n的二进制是 1111,&出来相当于取低四位。
8 楼 NanguoCoffee 2011-02-17  
Ulysses 写道
楼主把因果关系搞颠倒了吧。


不明白,怎么颠倒了?
不是因要求均匀分布,才要求数组长度为2次幂么?
7 楼 Ulysses 2011-02-17  
楼主把因果关系搞颠倒了吧。
6 楼 superobin 2011-02-17  
我觉得哈,仅有一个方面,就是分配平均。为啥分配平均?首先hashCode就是一个能基本保证散列后的数据均匀分布在int区间上的函数,而用一个平均分配的hashCode对table长度取余则可以另对象均匀的落在table中的每个链表中
假定 x 是2^n(n>0)那么 对任意整数i有
i&(x-1) === i%x
而&与%效率差距实在太大了
我之所以知道这个是因为看过访谈hashmap的作者,貌似他就是这个意思,原帖翻不到了
这个东西同样也解开了我对md5碰撞算法时那帮科学家写的i&3的疑惑


另外,map中有个void transfer(Entry[] newTable)方法,重新将old entrys逐链表扔进新的数组,故capalicy为2^n和扩容没有直接关系

5 楼 CoderPlusPlus 2011-02-17  
ls的两句话不就是同一个意思么
4 楼 pengmj 2011-02-17  
HashMap的算法有两个目的:1、尽量均匀分配;2、在一个节点上的元素尽可能少。
3 楼 NanguoCoffee 2011-02-17  
发现我的SplitReentrantLock的index(String key)改成另外一种算法,就可以避免hash分布不均匀。
 int index(String key) {
	int hash = hash(key.hashCode());
	hash = Math.abs(hash);
	return hash % LOCK_NUM;
}


使用取余操作 代替 与 操作。

优点:使用非2次幂的长度也能够hash均匀
缺点:多了一次Math.abs(hash),并且取余的效率比与运算的效率低

因此大并发使用的情况下,还是使用与操作比较好。普遍性来说还是取余比较好。
2 楼 NanguoCoffee 2011-02-16  
kimmking 写道
还有一个原因,扩容后,原有hash不变

其实还有一个方法可以满足这两个要求:使用质数。(例如.net的hashtable中当前用的是p,下个数则用小于2p的最大的质数)


hash不变是
 
  static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

得到的,不管是否扩容,对于指定的key来说,得到的值都是不变的。

因此扩容之后得到数组下标和扩容之前有可能是不一样的
1 楼 kimmking 2011-02-16  
还有一个原因,扩容后,原有hash不变。

其实还有一个方法可以满足这两个要求:使用质数。(例如.net的hashtable中当前用的是p,下个数则用小于2p的最大的质数)

相关推荐

    Jdk1.8中的HashMap实现原理.pdf

    需要注意的是,HashMap的数组长度必须是2的幂次方,这样可以通过位运算快速计算出索引位置,提高效率。而且,由于哈希桶数组的大小是合数,而不是通常认为更利于避免冲突的素数,这在一定程度上是因为转换为红黑树时...

    Jdk1.8中的HashMap实现原理.docx

    数组的大小必须是2的幂,这是为了简化哈希索引计算,确保每个键值对可以通过哈希值均匀分布在整个数组中。哈希碰撞,即多个键值对的哈希值相同,HashMap通过链表解决,将这些节点链接在一起。在Jdk1.8中,当链表长度...

    源码解析jdk7.0集合(3):HashMap的底层实现原理.pdf

    数组长度的默认值为16,并且数组的长度必须是2的n次幂。这种长度要求是为了保证散列均匀性,避免哈希冲突,同时也是为了优化性能。 #### 三、HashMap内部实现原理机制 在JDK 7.0中,HashMap的内部结构由以下几个...

    HashMap的实现原理

    在JDK 1.8中,由于哈希表的大小通常是2的幂,所以`(tableSize - 1)`结果的二进制形式中所有位都是1,这样做的目的是为了将哈希值映射到数组的正确索引。此外,`key.hashCode()&gt;&gt;&gt;16`是为了合并高16位和低16位,减少...

    fixed-size-hashmap:固定大小的哈希映射,仅使用基本类型将字符串键与任意数据对象相关联

    原始固定大小的HashMap 一个固定大小的哈希映射,仅使用基本类型将字符串键与任意数据对象相关联。 时间复杂度: 案件... 内部HashMap数组:内部哈希表数组中的“存储桶”数量固定为2的最小幂,大于“ n”,以通过位

    Jdk1.8 HashMap实现原理详细介绍

    在HashMap中,哈希桶数组`table`的长度必须是2的幂,这是因为哈希算法计算出来的索引是通过与运算(`&`)和位移运算来完成的,这样的设计使得哈希值的计算更高效,同时减少了冲突的可能性。与素数相比,虽然合数可能...

    Java中的哈希表

    HashMap的数组长度必须始终为2的幂次方,这是为了确保散列函数的均匀性以及散列冲突的最小化。数组中的每一个元素(Entry)不仅持有键值对信息,还包含一个指向下一个元素的引用,形成了链式结构。当执行put操作时,...

    面试官!你又双叒叕问HashMap!

    HashMap在初始化时要求容量必须是2的幂,例如16、32等。这是为了保证哈希函数的效率。如果容量不是2的幂,某些哈希值会均匀分布,而其他则会集中在一个较小的范围内,导致性能下降。2的幂使得`(hash & (length - 1))...

    JAVA HashMap详细介绍和示例

    - `HashMap(int capacity)`:指定初始容量的构造函数,若容量值不是2的幂,HashMap会自动调整到最近的2的幂。 - `HashMap(int capacity, float loadFactor)`:同时指定容量和加载因子。 - `HashMap(Map&lt;? extends K,...

    哈希表采用何种算法计算出hash 值?还可以用哪些方法计算?.pdf

    4. 当插入新键值对时,如果当前哈希表的元素数量`size`超过其容量`capacity`的75%,即`size &gt; capacity * 0.75`,HashMap会进行扩容。扩容后的新容量通常是原容量的2倍,以保持负载因子(元素数量与容量的比例)接近...

    Java基础学习23.pdf

    HashMap的容量通常是2的幂,初始容量为16,最大容量为2的30次方。负载因子默认为0.75,当元素数量达到容量与负载因子的乘积时,HashMap会进行扩容。扩容时,旧数组中的元素会被重新哈希并分布到新的更大的数组中。 ...

    Java集合容器面试题(2022最新版)-重点.docx

    #### HashMap的长度为什么是2的幂次方 - 为了优化哈希值的计算,使用位运算代替取模运算可以提高效率。 - 这样可以使容量始终保持为2的幂次方,从而简化内部的计算逻辑。 #### HashMap与HashTable的区别 - `HashMap...

    Map实现类1

    默认负载因子是0.75,初始容量是16,容量必须是2的幂。 - 主要方法:包括put、get、remove、containsKey、containsValue、size等,以及清空和复制Map的方法。 2. TreeMap - 数据结构:TreeMap使用红黑树(一种自...

    ConcurrentHashMap之实现细节

    - **重新哈希**:尽管默认情况下散列表的大小为2的幂次方,但在某些情况下(例如并发级别较低时),可能会导致数据分布不均。为了避免这种情况,`ConcurrentHashMap`会在必要的时候重新哈希,以确保数据能够更均匀地...

    java程序设计期末习题集.doc

    `add(new String("ABC"))`添加了一个不同的对象,所以最终`set.size()`为2。 3. **集合接口**:Java的集合框架包括多个主要接口,如`Collection`(所有集合的顶级接口),`List`(有序集合,允许重复元素),`Set`...

    HashTableProblem

    1. `容量`(Capacity):哈希表数组的大小,通常是2的幂次方。 2. `负载因子`(Load Factor):哈希表在达到多大比例的填充率时需要进行扩容。默认为0.75,意味着当表中元素数量达到容量的75%时,会自动扩容。 3. `...

    java面试题及答案-非常全面(包括基础、网络、数据结构、算法及IT大厂面经)

    - **初始容量大小**:`HashMap`默认初始容量为16,而`HashTable`默认初始容量为11。 - **扩容方式**:`HashMap`的扩容是在原有基础上翻倍再加1;`HashTable`的扩容是翻倍。 - **null键值**:`HashMap`可以接受一个...

    LinearProbingHash:使用哈希表的线性探测实现

    - **二次探测**:不是线性地检查下一个槽位,而是按照2的幂次方步长检查,例如检查`hash + 1`, `hash + 2^1`, `hash + 2^2`等,这样可以避免形成连续的聚集。 - **再哈希**:当探测到一定次数后,使用另一个不同的...

Global site tag (gtag.js) - Google Analytics