一、HashMap的定义
HashMap是存放key,value键值对的数据结构,是由数组和链表(红黑树)组成的。
二、HashMap的put实现过程
- 计算Hash值,找到存放的节点,位置为hash%length。由于HashMap的长度总是2的N次方,所以位置可以由hash & (length-1)来计算。
- 如果存放的节点没有值,就在该位置上生成一个新的节点(Entry或者Node),如果有值了,则需要分情况讨论。
- jdk1.7:先判断是否需要扩容,不扩容就生成Entry对象,使用头插法添加到链表。
- jdk1.8:
- 先判断节点的类型是链表还是红黑树。
- 如果是红黑树,会将该Node添加到红黑树中。或者存在相同的key就更新。
- 如果是链表,使用尾插法添加到链表。或者存在相同的key就更新。如果插入以后当前的结点个数大于等于8,则将该链表转换为红黑树。
- 插入完Node之后,会判断是否元素个数达到阈值,达到就进行扩容。
- 先判断节点的类型是链表还是红黑树。
三、HashMap是怎么扩容的
- 生成原始容量*2的新数组
- 遍历老数组的每一个元素
- 重新计算各元素的下标转移到新数组
- jdk1.7: 直接将元素添加到新数组
- jdk1.8: 先判断节点是链表还是红黑树
- 如果是链表,记录lo和hi两个链表,分别表示新位置在原处的和新位置不在原处的(新位置=旧位置+旧容量),然后分别修改新数组位置的节点。
- 如果是红黑树,记录lo和hi两个树,也是表示新位置在原处和不在原处的,并且记录每个树的个数,如果最后的个数<=6,就将树转为为链表,最后修改新数组位置的结点。
四、HashMap为什么1.7用头插法,1.8改为尾插法
jdk1.7中插入的方式是头插法,Java1.8改成了尾插法。
- 因为多线程时,头插法在HashMap需要扩容的时候会造成环状链表,尾插法可以保证原有的顺序,所以之后使用尾插法了。
- 造成环状链表的原因是:
- 假设某个位置的链表有A、B两个节点,当第一个线程记录了当前节点A和next节点B就被阻塞了。
- 第二个线程对HashMap进行扩容,扩容结束后变成了B->A。
- 第一个线程继续进行扩容,先插入A、再插入B,由于B的next节点是A(本来是null),所以继续循环又插入A。这时A的next是B,B的next是A,造成了循环链表。
五、多线程的情况
虽然jdk1.8之后改为尾插法不会造成环状链表了,但这个方法并不是一个线程安全的类,没有给put/get加锁,会有并发问题。
HashTable和ConcurrentHashMap是线程安全的。
(一)HashTable和HashMap的区别
- 实现方式
Hashtable继承的是Dictionary类,HashMap继承的是AbstractMap类 - 线程安全
HashTable是线程安全的,对数据操作的所有方法都上锁(synchronized),所以效率较低 - 迭代器
HashTable是fail-safe,HashMap是fail-fast。 - null
HashTable不允许键或值为null,HashMap可以。HashTable put空值时会直接抛空指针异常,但是HashMap做了特殊处理,会认为hashcode=0。
因为Hashtable使用的是安全失败机制,读到的数据不一定是最新数据。如果使用null值,无法判断对应的key是不存在还是为空。 - 初始化容量
HashTable为11,HashMap为16 - 扩容机制
Hashtable翻倍+1,HashMap翻倍
(二)ConcurrentHashMap
jdk1.7
- 1.7采用分段锁,数据结构是Segment数组+HashEntry数组
- Segment继承了ReentrantLock,一个Segment对象可能守护多个HashEntry,HashEntry的next、value由volatile修饰。
- 由于只锁住了数组的一个位置,每当一个线程访问Segment时,不会影响其它位置,并发性更好。
- 在进行put时,先自旋获取锁;如果重试次数达到阈值,就改为阻塞锁,保证能获取成功。
- 在进行get时,由于HashEntry的Value是由volatile修饰,保证了内存可见性,获得的是最新值。这个过程不需要加锁。
jdk1.8
- 1.8抛弃了原有的Segment分段锁,采用CAS+synchronized保证并发安全
- HashEntry改为Node,next和value仍然由volatile修饰
- 在进行put时
- 如果当前位置为空,使用CAS尝试写入,失败则自旋保证成功
- 如果当前位置需要扩容,则扩容
- 都不满足,则利用sychronized锁写入数据(锁的是当前位置的第一个Node)
- 如果链表长度大于阈值,则转换为红黑树。
- 在进行get时,没有加锁,也是用volatile保证可见性,直接返回。
总的来说:
- 锁的粒度更小,1.7锁住多个HashEntry的Segment,1.8锁住单个HashEntry(Node)
- 1.8用CAS+sychronized,更轻量,特别是后者经过了锁升级。