HashMap的工作原理

Posted yewu123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap的工作原理相关的知识,希望对你有一定的参考价值。

1、我们为什么使用hashmap?

a. hashmap是线程不安全的

b. hashmap存储很快
c.hashmap是以键值对的形式存在的
d.hashmap的key和value都能接受null值
原因无非就是以上这么多
2、HashMap是什么?
HashMap其实就是数组+链表的结合体,HashMap的底层就是一个数组,其数组的每一个元素又是一个链表,每一个元素存放数组的头节点,当新建一个HashMap的时候,就会初始化一个数组。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。
一个ConcurrentHashMap里包含一个Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的segment锁。

3、HashMap的工作原理是什么?
HashMap是基于hashing的原理,我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,先对key调用hashcode方法,来计算hash值,返回的hash值用来找bucket对象,来放entry键值对。
HashMap的数据结构
/
The table, resized as necessary. Length MUST Always be a power of two.
/
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V>
final K key;
V value;
Entry<K,V> next;
final int hash;
...

3.1HashMap存储函数的实现(k key,v value)
根据下面put方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时
a、程序首先计算该key的hashcode()值
b、然后对该哈希码值再哈希
c、然后把哈希值和(数组长度-1)进行按位与操作,得到存储的数组下标
d、如果该位置处设有链表节点,那么就直接把包含<key,value>的节点放入该位置。如果该位置有节点,就对链表进行遍历,看是否有hash,key和要放入的节点相同的节点,如果有的话,就替换该节点的value值,如果没有相同的话,就创建节点放入值,并把该节点插入到链表表头(头插法)。

public V put(K key, V value)
//HashMap允许存放null键和null值。
//当key为nu11时, 调用putForNullKey方法,将valve放置在数组第一个位置。
if (key = null)
return putForNullKey(value);
//根据key的keyCode重新计算hash值。
int hash = hash(key .hashCode());
//搜索指定hash值在对应table中的索
int i = indexFor(hash, table . length);
//如果i索引处的Entry不为null,通过循环不断遍历e元素的下一个元素。
for (Entry<K,V> e = table[i]; e != null;e = e.next)
Object k;
if (e.hash == hash && ((k == e.key) == key || key. equals(k)))
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;


//如果读引处的Entry为null,表明此处还没有Entry
omodCount++;
//将key、 value添加到索引处。
addEntry(hash, key, value, i);
return null;

void addEntry(int hash, K key, V value, int bucketIndex)
//获取指定bucketIndex 索引处的Entry
Entry<K,V> e = table[bucketIndex];
//将新创健的Entry 放入bucketIndex索引处,并让新的Entry 指向原来的Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//如果Hap中的key-value对的数里超过了极限
if (size++ >= threshold)
//把table对象的长度扩充到原理的2倍
resize(2*table.length);

二倍扩容
void resize(int newCapacity)
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY)
threshold = Integer.MAX_VALUE;
return;

Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

void transfer(Entry[] newTable, boolean rehash)
int newCapacity = newTable.length;
for (Entry<K,V> e : table)
while(null != e)
Entry<K,V> next = e.next;
if (rehash)
e.hash = null == e.key ? 0 : hash(e.key);

int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;


static int indexFor(int h, int length)
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);

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

扩展:为何数组的长度是2的n次方呢?
a、这个方法非常巧妙,它通过h & (table.length-1) 来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,2^n-1得到的二进制数的每个位上的值都为1,那么与全部为1的个数进行与操作,速度会大大提升。
b、当length总是2的n次方时,h & (length -1)运算等价,对length取模,也就是h%length,但是&对%具有更高的效率。
c、当数组长度为2的n次幂的时候,不同的key算得的index相同的几率较小,那么数据在数组上分布就比较均匀,也就是碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样的查询效率也就比较高了。
3.2HashMap读取函数的实现get
public V get(object key)
if (key = null)
return getForNullKey();
int hash ”hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table. length)];
e!=null;
e = e.next)
0bject k;
if (e.hash=hash && ((k=e.key)==key II key.equals(k))
return e.value;

return null;

HashMap的get方法
方法1、首先通过key的两次hash后的值与数组的长度-1进行与操作,定位到数组的某个位置,
2、然后对该列的链表进行遍历,一般情况下,hashMap的这种查找速度是非常快的,hash值相同的元(O就会造成链表中的数据很多,而链表中的数据查找是通过遍历链表中的元素进行的,这可能会影响到查找速度,找到即返回。特别注意:当返回为null值时,你不能判断是没有找到指定元素,还是在hashmap中存着一个value为null的元素,因为hashmap允许value为null)

HashMap的扩容机制:
当HashMap中的结点个数超过数组大小laodEactor(加载因子)时,就会进行扩容,loadFactor的默认值为0.75
也就是说,默认情况下,数组大小为16,那么当HashMap节点个数超过160
0.75=12的时候,就把数组的大小变为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,并放进去,而这是一个非常消耗性能的操作。

多线程下HashMap出现的问题:

1、多线程put操作后,get操作导致死循环导致cpu100%的现象。主要是多线程同时put时,如果同时触发了rehash操作,会导致扩展后的HashMap中的链表出现循环节点进而使得后面get的时候,会死循环。
2、多线程put操作,导致元素丢失,也是发生在个别线程对hashmap扩容时。
HashTable的原理
它的原理和hahsMap基本一致

HashMap和HashTable的区别
1、Hashtable是线程安全的,方法是Synchronized的,适合在多线程环境中使用,效率稍低;HashMap不是线程安全的,方法不是synchronized的,效率稍高,适合在单线程环境下使用,所以在多线程场合下使用的话,需要手动同步HashMap,Collections,synchronizedMap()。

HashTable的效率比较低的原因?

在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,访问其他同步方法的线程可能会进入阻塞或者轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且不能使用get方法来获取元素,所以竞争越激烈效率越低。

2、HashMap的key和value都可以为null值,HashTable的key和value都不允许为null值。
3、HashMap中数组的默认大小为16,而且一定是2的倍数,扩容后的数组长度是之前数组长度的2倍。HashTable中数组默认大小是11,扩容后的数组长度是之前数组长度的2倍+1.
4、哈希值的使用不同
而HashMap重新计算Hash值,而且用&代替求模:
int hash = hash(key.hashcode0);
int i= indexFor(hash, table.length);
static int hash(Objectx)
int h = x.hashCode();
h += ~(h<<9);h^= (h>>> 14);
h+=(h<< 4);
h ^= (h>>> 10);
returm h;

static int indexFor(int h, int length)
return h & (length-1); //hashmap的表长永远是2^n。

HashTable直接使用对象的hashcode值:

判断是否含有某个键

在hashmap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null,当get()方法返回null值时,既可以表示HashMap中没有该键,也可以表示该键所对应的值为null。因此,在hashMap中不能用get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。Hashtable的键值都不能为null,所以可以用get()方法来判断是否含有某个键。

参考:
https://www.jb51.net/article/154565.htm
http://www.jiaochengku.net/ITjiaocheng/ruanjianbiancheng/2538.html

以上是关于HashMap的工作原理的主要内容,如果未能解决你的问题,请参考以下文章

HashMap工作原理及实现

HashMap的工作原理

HashMap----工作原理

Java HashMap工作原理及实现

HashMap----工作原理

Java HashMap工作原理及实现