1. HashMap概述
HashMap基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用null值和null键。(除了不同步和允许使用null之外,HashMap类与HashTable大致相同。)HashMap不保证映射的顺序,特别是它不保证该顺序恒久不变。HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collection类的静态方法synchronizedMap获得线程安全的HashMap。Map map = Collection.synchronizedMap(new HashMap());
2. HashMap的数据结构
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为通过计算散列码来决定存储位置。HashMap中主要是通过Key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。HashMap底层通过链表来解决hash冲突。
图中0-15部分代表哈希表,也称为哈希数组,数组的每个元素都是单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处就将其放入单链表中。这些元素通过hash(key)%len也就是元素的key的哈希值对数组长度取模得到。比如上述的哈希表中,12%16=12,28%12=12,108%16=12所以12,28,108都存储在数组下标为12的位置。
HashMap其实是一个线性的数组实现的,首先HashMap里面实现一个静态内部类Entry,其重要的属性有key,value,next.Entry就是HashMap键值对实现的一个基础bean,这个线性数组就是Entry[ ],Map里面的内容都存在Entry[ ]中。Next也是一个Entry对象用来处理hash冲突的形成一个链表。
3.HashMap源码分析
关键属性:
loadFactor加载因子是表示Hash表中元素的填满的程度。默认值为0.75。加载因子越大填满的元素越多,好处是空间的利用率高了但冲突的机会加大了。链表的长度会越来越长查找效率降低。
构造方法:
默认初始容量为16,默认加载因子为0.75。
存储数据:
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
//搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
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))) { //如果key相同则覆盖并返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改次数+1
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
4.调整大小Resize、
voidresize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY){
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = newEntry[newCapacity];
transfer(newTable);//用来将原先table的元素全部移到newTable里面
table = newTable; //再将newTable赋值给table
threshold = (int)(newCapacity *loadFactor);//重新计算临界值
}
当HashMap中的元素越来越多时hash冲突的几率也就越来越高,因为数组的长度时固定的,所以为了提高查询的效率就要对HashMap数组进行扩容。先将HashMap的全部元素添加到新的HashMap中并重新计算元素在新的数组中的索引的位置。
当hashMap中元素个数超过数组大小时,就会进行数组扩容,LoadFactor的默认值为0.75,数组大小为16那么当HashMap中元素个数超过16*0.75即12的时候就把数组扩容。扩容时原数组的数据必须重新计算其在新数组中的位置并放进去。
5.数据读取
从HashMap中获取数据时首先计算Key的hashcode找到数组中对象位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
6.HashMap为啥初始化容量为2的次幂
staticint indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 :"length must be a non-zero power of 2";
return h & (length-1);
}
H为插入元素的hashcode,length为map的容量大小。如果length为2的次幂,则length-1转化为二进制为11111…的形式,在与h的二进制与操作效率会非常快,而且不浪费空间;如果length不是2的次幂比如length为15,则length-1为14,对应的二进制为1110在与h与操作最后一位都为0,而0001,0011这样的位置都永远不会存放元素了,造成空间的浪费,更糟的是数组使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率。
7.HashMap、ConCurrentHashMap和HashTable的比较
HashMap是非线程安全的,HashTable是线程安全的;HashMap的键和值都允许有null存在,而HashTable则都不行;因为线程安全、哈希效率的问题,HashMap效率比HashTable的要高。ConCurrentHashMap是线程安全的HashMap的实现,HashTable里使用的是synchronized关键字,这其实是对对象的加锁,锁住的是对象整体。ConcurrentHashMap引入了分割,把一个大的Map拆分成几个小的HashTable,在内部使用的同步机制是基于lock操作的,这样可以对Map的一部分进行上锁,这样影响的只是将要放入同一个Segment的元素的put操作,保证同步的时候锁住的不是整个Map,相对于HashTable提高了多线程环境下的性能,因此HashTable已经被淘汰了。ConcurrentHashMap对整个桶数进行了分割分段,然后再每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁不是线程安全的;HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
8.HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患?
HashMap事实上并非线程安全的,在高并发的情况下是非常可能发生死循环的,由此造成CPU 100%。所以在多线程的情况下用HashMap是非常不妥当的行为,应采用线程安全类ConcurrentHashMap取代。HashMap在进行存储时当size超过当前最大容量负载因子时会发生resize,在resize时每个链表转化成新表,而且链表中的位置发生反转,这在多线程情况下非常容易造成链表回路。