HashMap
jdk1.7与1.8的共同点和差异点
共同点:
- 线程不安全
- 每次put时,都会判断是否需要扩容。
- hashmap的容量只能是2的n倍,当初始化不为2的倍数时,默认会创建大于该初始值的2的倍数容量大小。为了减少hash碰撞与提高运算性能用位运算。
- 属性:默认初始化容量为16,负载因子为0.75(即容量为12=16*0.75会自动扩容)
差异点:
jdk1.7——
- 数据结构结合了数组和链表的特点,默认为size为16的entry[],当发生hash冲突时,在对应entry下形成链表结构。链表的时间复杂度为O(n)
- 添加数据时先扩容后添加。
- 添加数据的时候,采用头插法进行添加(方便查询,链表查询时会先二分,之后分别进行遍历,越靠前越容易查询)。并发情况下,会产生链表死循环。
- 当第一次添加的时候,进行空间分配数组填充。
- 扩容时,hash重新计算。
jdk1.8——
- 数据结构结合了数组和链表的特点,默认为size为16的Node[],当发生hash冲突时,在对应Node下形成链表结构。当链表过长,即长度大于8时。1.判断是否需要转换成红黑树,转换红黑树前,再判断Node数组与MIN_TREEIFY_CAPACITY=64比较,如果小于,先进行2倍扩容解决,大于,则进行红黑树转换。链表的时间复杂度为O(n),红黑树的时间复杂度为O(logn)。
- 添加数据时先添加再扩容。
- 添加数据采用尾插法,红黑树也可能会产生死循环。并发情况下,计算size大小时不安全,hash冲突计算前后并发可能发生覆盖数据。
- 当第一次添加时,直接先扩容。
- 使用高低位索引机制计算hash,hash算法更散列,减小hash冲突。
- 扩容时,hash二进制多取一位,即扩容后,链表上的元素,要么在本索引下,要么在该索引+原数组长度,减少了hash次数,提升性能。
引申
为啥要链表长度大于8且数组长度大于64才进行树化?
链表节点太少没必要转换数据结构,因为转化的过程需要耗费时间与空间。
为什么不直接一直用红黑树呢?
因为树的结构太浪费空间,只有节点足够多的情况下用树才能体现出它的优势,而如果在节点数量不够多的情况下用链表的效率更高,占用的空间更小。
ConcurrentHashMap
jdk1.7与1.8的共同点和差异点
共同点:
- 线程安全,具备hashMap的特性
差异点:
jdk1.7——
- ReentrantLock+Segment+HashEntry的结构
- 锁的粒度基于segment
- segment默认容量为16
jdk1.8——
- synchronized+CAS+HashEntry+红黑树
- 锁头Node
- 使用红黑树优化链表,同hashmap
- 使用synchronized代替reetrantLock
引申
为什么使用synchronized代替reetrantLock
- reetrantLock的优点是灵活,通过Condition来调控;而在低粒度的锁中,Condition相比于synchronized没有优势
- 在jdk团队的升级维护下,使用了锁升级的优化方式,一步步的从偏向锁到轻量级锁到最后的重量级锁,synchronized的性能不弱于reetrantLock,在低粒度的锁下有更好的表现