关于HashMap底层原理
首先了解到 HashMap 是用来存贮一个个 <key,value>(<键,值>)的集合,实则 HashMap 则为一个数组,而这种 <key,value> 对则为存在数组中某一位置的值;
1、那么每个 <key,value> 对是存于什么位置呢?原来在将每个 <key,value> 存入前,还利用一个哈希函数来计算位置 index = Hash(key); 然后按照所得位置在数组存入 <key,value> 对。那么插入过多的 <key,value> 对时,则必然会出现 index冲突 的情况,这时就需要数据结构中的 拉链法 来处理冲突问题,既每当出现index冲突问题时,将新 <key,value> 对插入对应位置,此位置上的原 <key,value> 弹出,新 <key,value> 对通过指针指向原 <key,value> 对,从而达到链表的效果并同时解决了插入index冲突问题。
2、那么当取value时用的GET(key)方法如何获取value的呢?当使用Get(key)方法取值时,会通过所填key计算 index = Hash(key),然后在对应位置上的链表进行检索符合键值为所填key的<key,value>对并返回value的值。
注:
1、HashMap 默认长度16,并且每次扩展或手动初始化时长度必须为2的幂;
2、index = HashCode(Key) & (Length - 1) (&为二进制位运算);
3、默认长度为16的好处,则是使插入的<key,value>对分布均匀。
关于HashMap的死锁
hashmap是单线程安全的,挡在并发多线程情况下,易出现死锁情况,原因:
首先hashmap既是一个table[];当hash(key)超出原有范围时,便会扩容出发rehash
;在单线程情况下,此操作是安全的,并且若原hash表中链表为A->B->C;那么转入新表中时,可能为C->B->A,既顺序会变反;
在rehash时会出发的一个关键方法(transfer),见源码:
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;
}
}
分析:e节点为当前遍历链表的第一个节点,用next保存e.next;接着,e.next指向newtable[i]的第一个节点;将e插入newtable[i]的链表的头部,接着e = next并继续循环;如此,就将当前链表转移进了新的hashmap,且容易明白为什么插入新表后顺序会相反;
那么死锁问题就出现在了这里,当多线程访问时,假设当前有两个线程同时插入数据,并都需要进行rehash,则当第一个线程rehash时并进行到next = e.next时,线程调度到线程2,则线程1暂时挂起;当线程2完成rehash建成新hashmap2时,此时,e与next皆指向的是新hashmap2中的节点,且next.next = e;则线程一继续执行,此时当执行完第一个转移时,e = next ; next = e.next ;既原有e 与 next 相互将换并继续执行循环,不难看出此时已出现环状链表;故此新建hash表进行get()时,则会出现死锁的状况;
解决:使用ConCurrentHashMap
有序的Map类
TreeMap和LinkedHashmap都是有序的。(TreeMap默认是key升序,LinkedHashmap默认是数据插入顺序)
TreeMap是基于比较器Comparator来实现有序的。
LinkedHashmap是基于链表来实现数据插入有序的。