关于HashMap的一些总结

HashMap的结构

数组的寻址快,但是数据的插⼊与删除速度不⾏。 链表的插⼊与删除速度快,但是寻址速度不⾏。 那
有没有⼀种两者兼具的数据结构,答案肯定是有的,那就是 hash 表。 HashMap 就是根据 数组 + 链表的
⽅式组成了 hash 表:
 

对于HashMap的⼀些疑问

⼀、HashMapresize过程是什么样的?

HashMap put 的时候会先检查当前数组的 length, 如果插⼊新的值的时候使得 length > 0.75f * size f
为加载因⼦,可以在创建 hashMap 时指定)的话,会将数组进⾏扩容为当前容量的 2 倍。 扩容之后必定
要将原有 hashMap 中的值拷⻉到新容量的 hashMap ⾥⾯, HashMap 默认的容量为 16 ,加载因⼦为
0.75 , 也就是说当 HashMap Entry 的个数超过 16 * 0.75 = 12 , 会将容量扩充为 16 * 2 = 32 ,然后 重新计算元素在数组中的位置,这是⼀个⾮常耗时的操作,所以我们在使⽤ HashMap 的时候如果能预
先知道 Map 中元素的⼤⼩,预设其⼤⼩能够提升其性能。 resize 代码:
//如果当前的数组⻓度已经达到最⼤值,则不在进⾏调整
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//根据传⼊参数的⻓度定义新的数组
Entry[] newTable = new Entry[newCapacity];
//按照新的规则,将旧数组中的元素转移到新数组中
transfer(newTable);
table = newTable;
//更新临界值
threshold = (int)(newCapacity * loadFactor);
}
//旧数组中元素往新数组中迁移
void transfer(Entry[] newTable) {
//旧数组
Entry[] src = table;
//新数组⻓度
int newCapacity = newTable.length;
//遍历旧数组
for (int j = 0; j < src.length; j++) {
Entry e = src[j];
if (e != null) {
src[j] = null;
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity);//放在新数组中的index位置
e.next = newTable[i];//实现链表结构,新加⼊的放在链头,之前的的数据放在链尾
newTable[i] = e;
e = next;
} while (e != null);
}}
 
这是 1.7 中的代码, 1.8 中引⼊了红⿊树的概念,代码会相对复杂⼀些。
 

⼆、HashMap在扩容的时候为什么容量都是原来的2倍,即容量为2^n

HashMap 在计算数组中 key 的位置时,使⽤的算法为:
/* * Returns index for hash code h. */
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); }
即对 key hashcode 与当前数组容量 -1 进⾏与操作
我们假设有⼀个容量为分别为 15 16
hashMap ,有两个 key hashcode 分别为 4 5 ,进⾏ indexFor 操作之后:
H & (length -1) hash & table.length-1 4 & (15 - 1) 0100 & 1110 = 0100 5 & 15 -1 0101 & 1110
= 0100
4 & (16 - 1) 0100 & 1111 = 0100 5 & 16 -1 0101 & 1111 = 0101
我们能够看到在容量为 16 时进⾏ indexFor 操作之后获得相同结果的⼏率要⽐容量为 15 时的⼏率要⼩,这
样能够减少出现 hash 冲突的⼏率,从⽽提⾼查询效率。 2 ^ n 是⼀个⾮常神奇的数字。
 

三、put时出现相同的hashcode会怎样?

hashMap ⾥⾯存储的 Entry 对象是由数组和链表组成的,当 key hashcode 相同时,数组上这个位置存
储的结构就是链表,这时会将新的值插⼊链表的表头。进⾏取值的时候会先获取到链表,再对链表进⾏
遍历,通过 key.equals ⽅法获取到值。( hashcode 相同不代表对象相同,不要混淆 hashcode equals
⽅法) 所以声明作 fifinal 的对象,并且采⽤合适的 equals() hashCode() ⽅法的话,将会减少碰撞的发
⽣,提⾼效率。不可变性使得能够缓存不同键的 hashcode ,这将提⾼整个获取对象的速度,使⽤
String Interger 这样的 wrapper 类作为键是⾮常好的选择。

四、什么是循环链表?

HashMap 在遇到多线程的操作中,如果需要重新调整 HashMap 的⼤⼩时,多个线程会同时尝试去调整
HashMap 的⼤⼩,这时处在同⼀位置的链表的元素的位置会反过来,以为移动到新的 bucket 的时候,
HashMap 不会将新的元素放到尾部(为了避免尾部遍历),这时可能会出现 A -> B -> A 的情况,从⽽出
现死循环,这便是 HashMap 中的循环链表。 所以 HashMap 是不适合⽤在多线程的情况下的,可以考
虑尝试使⽤ HashTable 或是 ConcurrentHashMap

五、如何正确使⽤HashMap提⾼性能

在设置 HashMap 的时候指定其容量的⼤⼩,减少其 resize 的过程。
Version:0.9 StartHTML:0000000105 EndHTML:0000010584 StartFragment:0000000141 EndFragment:0000010544

六、HashMap HashTableConcurrentHashMap的区别

1.HashTable 的⽅法是同步的,在⽅法的前⾯都有 synchronized 来同步, HashMap 未经同步,所以在多
线程场合要⼿动同步
2.HashTable 不允许 null (key value 都不可以 ) ,HashMap 允许 null (key value 都可以 )
3.HashTable 有⼀个 contains(Object value) 功能和 containsValue(Object value) 功能⼀样。
4.HashTable 使⽤ Enumeration 进⾏遍历, HashMap 使⽤ Iterator 进⾏遍历。
5.HashTable hash 数组默认⼤⼩是 11 ,增加的⽅式是 old*2+1 HashMap hash 数组的默认⼤⼩是
  16 ,⽽且⼀定是 2 的指数。
6. 哈希值的使⽤不同, HashTable 直接使⽤对象的 hashCode ,⽽ HashMap 重新计算 hash值,
7.ConcurrentHashMap 也是⼀种线程安全的集合类,他和 HashTable 也是有区别的,主要区别就是加锁
的粒度以及如何加锁, ConcurrentHashMap 的加锁粒度要⽐ HashTable 更细⼀点。将数据分成⼀段⼀
段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据的时候,其他段的数据
也能被其他线程访问。

七、ConcurrentHashMap Hashtable 的区别

ConcurrentHashMap Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
底层数据结构:
JDK1.7 ConcurrentHashMap 底层采⽤ 分段的数组 + 链表 实现, JDK1.8 采⽤的数据结构跟
HashMap1.8 的结构⼀样,数组 + 链表 / 红⿊⼆叉树。 Hashtable JDK1.8 之前的 HashMap 的底层数据
结构类似都是采⽤ 数组 + 链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存
在的;
实现线程安全的⽅式(重要):
JDK1.7 的时候, ConcurrentHashMap (分段锁) 对整个桶数组进⾏了分割分段 (Segment) ,每⼀把
锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问
率。(默认分配 16 Segment ,⽐ Hashtable 效率提⾼ 16 倍。) 到了 JDK1.8 的时候已经摒弃了
Segment 的概念,⽽是直接⽤ Node 数组 + 链表 + 红⿊树的数据结构来实现,并发控制使⽤
synchronized CAS 来操作。( JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优
化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,
只是为了兼容旧版本;
Hashtable( 同⼀把锁 ) : 使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法
时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使
put 添加元素,也不能使⽤ get ,竞争会越来越激烈效率越低。
 

九、HashMap 多线程操作导致死循环问题

在多线程下,进⾏ put 操作会导致 HashMap 死循环,原因在于 HashMap 的扩容 resize()⽅法。由于 扩容是新建⼀个数组,复制原数据到数组。由于数组下标挂有链表,所以需要复制链表,但是多线程操

作有可能导致环形链表。复制链表过程如下:

以下模拟 2 个线程同时扩容。假设,当前 HashMap 的空间为 2 (临界值为 1 ), hashcode 分别为 0
1 ,在散列地址 0 处有元素 A B ,这时候要添加元素 C C 经过 hash 运算,得到散列地址为 1 ,这时
候由于超过了临界值,空间不够,需要调⽤ resize ⽅法进⾏扩容,那么在多线程条件下,会出现条件竞
争,模拟过程如下:
线程⼀:读取到当前的 HashMap 情况,在准备扩容时,线程⼆介⼊
线程⼆:读取 HashMap ,进⾏扩容
线程⼀:继续执⾏
这个过程为,先将 A 复制到新的 hash 表中,然后接着复制 B 到链头( A 的前边: B.next=A ),本来
B.next=null ,到此也就结束了(跟线程⼆⼀样的过程),但是,由于线程⼆扩容的原因,将
B.next=A ,所以,这⾥继续复制 A ,让 A.next=B ,由此,环形链表出现: B.next=A; A.next=B

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值