Java集合框架——Map

Map

以下类和接口,类与类之间的关系不是标准的,中间省去了很多不是太重要的接口和类。
在这里插入图片描述

线程安全

所有不同步的Collection和Map都可以通过Collections工具类下的synchrnoized系列方法使之成为同步容器,但这种方法的效率不如一些容器特定对应的同步容器效率高

  • xxx name = Collections.synchronizedxxx(new xxx)

HashMap

在这里插入图片描述

  • HashMap的底层实际上是数组+链表+红黑树(JDK1.8之后,JDK1.8之前只是数组+链表),数组的每个位置存的都是Entry链表,每一个位置又称为哈希桶。
  • 采用红黑树的原因是:当链表很长时,HashMap在get查找时,可能会遍历链表,这会造成O(n)的复杂度。而采用红黑树后,因为红黑树是平衡查找树,所以红黑树的查找是二分的,复杂度只有O(logn)
  • 空参HashMap容量是16,装填因子0.75,阈值是容量 * 装填因子,当HashMap容量大于阈值之后就会扩容,扩容为原来容量的2倍
  • 当链表里加入新Node之后,若此时链表长度 >= 8且HashMap总Entry数 > 64时,链表就转化成红黑树;当链表长度 >= 8但HashMap总Entry数 < 64时,只会进行HashMap的扩容;执行remove后,当红黑树长度 < 6时,当前红黑树又转化为链表
  • 不论给HashMap设置多大的初始容量,都会转变为大于等于设置的初始容量的最小2次幂数,比如设置了初始容量为23,其实底层会维护一个容量为32的HashMap,这是通过tableSizeFor方法完成的
  • 扩容后所有元素都要重新hash散列,所以这是很耗费性能的,所以我们初始化一个HashMap时,应充分考虑可能会有多少元素放到HashMap里,避免扩容。重新hash散列后,看元素新hash值新增的一位是1还是0,如果是0则元素新index与原来保持一致,如果是1则新index = 原index + 原容量大小
  • 将key的hashcode放入hash函数计算得到的hash值,该hash值 & (table.length - 1),即为在table中的下标,也就是Entry存储的位置
  • hash函数又称为“扰动函数”,是为了让元素散列的更平均。JDK1.8后,函数变为 hash(Object key) { int h = key.hashcode(); reutrn (key == null) ? 0 : h ^ (h >>> 16);},由此也可见,HashMap是允许null键的,且null键默认存在table的第一个位置
  • put元素的过程:1. 对key求hash值,计算出下标;2. 若当前下标位置没有元素则存入;3.若当前下标位置有元素,再用equals方法判断key是否是同一对象,若是同一对象,用新value覆盖旧value,若不是则发生hash碰撞,用拉链法解决hash碰撞,将新Entry用尾插法(JDK1.8之前是头插法)链在当前哈希桶的末尾;4.若链表程度 >= 8且HashMap总Entry数 > 64时,链表就是转化成红黑树;当链表长度 >= 8但HashMap总Entry数 < 64时,会进行HashMap的扩容;5.更新HashMap的size,若大于阈值,则扩容
  • get元素的过程:1.根据key求hash值,计算出下标;2. 看这个哈希桶里的头结点是不是该key,是就返回value,不是就按链表或者红黑树的方式继续向下搜索

HashMap线程不安全的原因:1.JDK1.8之前线程不安全发生在扩容的时候,具体说是发生在resize方法里的transfer方法里,由于扩容时涉及到链表的重新链接,如果有多个线程同时操作同一元素很可能造成环形链表和数据丢失;2.JDK1.8之后解决了上述问题(取消了transfer方法),但是还是线程不安全的,发生在put方法里,会导致数据覆盖的问题。具体是指:(线程A在判断完某个位置没有元素准备插入时时间片耗尽阻塞,这是线程B也来判断,发现该位置没有元素所以就插入了。此时线程A获得时间片恢复运行,因为之前已经做过判断操作了,所以直接将元素插入,导致线程B刚才插入的元素被覆盖了)。此外put方法里的++size更新容量的操作是非原子性的,所以这个操作也不是线程安全的。
为什么HashMap的容量是2的次幂:因为哈希表在散列元素时用的是除留余数法(index = hash % length),可是%操作比较耗时,设计者打算将%替换为&位运算以提高效率,而hash % length 和 hash & (length - 1) 只有在length是2的次幂数时才等价。所以设计者将HashMap的容量设定为2的次幂。
为什么每次扩容是2倍:因为要保证HashMap的容量是2的次幂。

LinkedHashMap

  • 继承自HashMap,拥有和HashMap几乎一致的特性,但是由于底层是双向链表,所以是有序的,构造参数可以设置一个布尔值指定迭代时元素输出的顺序

Hashtable

  • 老旧的线程安全的HashMap
  • 所有方法都加上了synchrnoized保证线程安全
  • 初始容量11,装填因子0.75,扩容为原来的2倍+1,使用拉链法(头插)解决Hash冲突,并没有采用红黑树

为什么Hashtable扩容是2倍+1而HashMap扩容是2倍:在Hashtable里求元素存的位置用的是除留余数法(index = hash % length),根据《算法导论》,这个hash是素数的时候,元素在哈希表里会分散的更为平均,由于初始容积是11,发现11*2+1 = 23,23*2+1 = 47 … 经过大量实验,发现*2+1这种方式得到素数的概率更大,所以采用了这种方式

ConcurrentHashMap

  • 线程安全的HashMap,具有和HashMap一样的参数
  • put过程:1. 使用与HashMap相同的方法算出下标;2.若该下标处无元素,则用CAS的方式插入;3.若当前map正在扩容,则当前线程先协助其他线程完成扩容,再插入;4. 若发生hash碰撞,则加上synchrnoized(锁粒度为一个Node)解决碰撞,解决方式与HashMap一样
  • get过程:与HashMap一样
  • JDK1.8之前,ConcurrentHashMap采用segment分段锁设计,使用ReentrantLock保证线程安全,默认将map分为16个segment,每个segment里是一个数组,锁粒度是segment;JDK1.8之后,synchrnoized经过优化,且因为锁粒度下降为node,即使发生线程争抢,使用CAS的方式自旋几十次也差不多能够解决问题了,synchrnoized根本升级不要重量锁,所以在JDK1.8以后,ConcurrentHashMap放弃segment设计,采用CAS+synchronized的方式实现线程安全。
  • 在这里插入图片描述
    在这里插入图片描述

ConcurrentSkipListMap

  • 单线程下实现自定义顺序用TreeMap,多线程下实现自定义顺序用ConcurrentSkipListMap
  • ConcurrentSkipListMap通过在结点上建立索引的方式快速实现插入、删除,redis底层使用了这一数据结构,每一个索引都维护了down和right指针
  • 在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

TreeMap

  • 底层使用红黑树进行排序,可以自己传入一个Comparator,重写compare方法
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值