Map
-
Map实现类保存的都是key-value对的
-
key是不可重复的,key的底层是Set集合,Set的一大特性就是具有去重功能
-
value是可重复的,value的底层是Collection集合,是允许重复数据保存的
-
用来标识map里的每项数据
HashMap、HashTable、ConcurrentHashMap
HashMap | HashTable | ConcurrentHashMap | |
---|---|---|---|
底层 | JAVA8前:数组+链表 JAVA8后:数组+链表+红黑树 | 8前:+分段锁 8后:+CAS+synchronized | |
数组长度默认16;存储的内容是链表的头节点 通过hash函数计算要存的位置,再遍历链表来获得元素,时间复杂度O(n) JAVA8之后,引入红黑树,通过常量TREEIFY_THRESHOLO判断是否将链表转化为红黑树来存储,阀值>8转红黑,<6转链表,时间复杂度O(logn) | 通过锁细粒度化优化 分段锁:将锁一段一段存储。segments默认长度为16,逻辑上将table数组拆分成多个子数组,每个子数组配置一把锁 table中每个bucket都用一把不同的锁来管理 | ||
构造函数 | 仅仅给一些变量赋值,采用Lacy_load的原则,在首次使用时才初始化 | ||
函数 | resize():具备初始化和扩容(2x)两个功能 hash():计算在数组中的位置 | ||
安全性 | 不安全 多线程下调用put触发调整大小时可能会存在条件竞争,容易导致死锁 | 安全 public方法都加入了synchronized修饰符,锁住整个对象 | 安全: 锁拆的更细,首先使用无锁操作CAS插入头节点,失败就循环重试 若头节点已存在,则尝试获取头节点的同步锁,再进行操作 |
Collections.synchronizedMap(hashMap),就可以将它包装成一个线程安全的map实例:mutex互斥对象成员,对其中的public方法加入synchronized对mutex进行加锁。串行效率低 | 不允许插入null键 串行效率低 | 不允许插入null键 sizeCtl标识符 串行效率高 |
HashMap的put方法
- 如果HashMap未被初始化过,则初始化
- 对Key求Hash值,然后再计算下标
- 如果没有碰撞,直接放入桶中
- 如果碰撞了,以链表的方式链接到后面
- 如果链表长度超过阀值8,就把链表转成红黑树
- 如果链表长度低于6,就把红黑树转回链表
- 如果节点已经存在就替换旧值
- 如果桶满了(容量16*加载因子0.75),就需要resize(扩容2倍后重排)
HashMap减小碰撞
- 扰动函数,使元素位置分布均匀,减小碰撞的几率
- 使用final对象作为key,采用合适的equals()和hashCode()方法,因为final的不可变性,能缓存不同键的hashcode,可以提高获取的速度,String、Integer
HashMap的hash
本质:使用 与操作来代替取模:
HashMap的扩容resize
-
默认负载因子是0.75,当一个map填满了75%的bucket时扩容
-
扩容:创建两倍的原来大小的数组来重新调整map的大小,并调用rehash将原来的对象放入到原来的bucket数组中
-
假设现在是4个元素,扩容一倍,变成8个元素,那么之前的元素可能要重新计算它在数组中的位置。
- 对于1来说,1在4个元素时排序在第1位,它在8个元素时还是排序在第一个。
- 对于6来说,它在4个元素时排在第2位,在8个元素时就需要排在第6位。6从2迁移到6,移动了4个位置,刚好是扩容一倍的长度。
- 所以,对于扩容2倍的情况来说,所有的元素,位置要么不变,要么是变化为旧的位置+扩容长度。当然,实际上这里的操作是二进制的位运算,这里只是拿十进制举例。
ConcurrentHashMap特性
-
大小控制标识符sizeCtl:
- 含义:-1正在初始化,-n表示有n-1个线程正在进行扩容操作,n或0表示的是初始化或者扩容的大小
- volatile修饰:多线程之间可见
-
不允许null key
-
CAS更新需要不断重试,直到成功为止
-
通过computeIfAbsent、compute可以构建java本地缓存
ConcurrentHashMap的put方法
-
判断Node[]数组是否初始化,没有则进行初始化操作
-
通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
-
检查到内部正在扩容,就帮助它一块扩容。
-
如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
-
如果是Node(链表结构)则执行链表的添加操作。
-
如果是TreeNode(树型结构)则执行树添加操作。
-
判断链表长度已经达到临界值8,当节点数超过这个值就需要把链表转换为树结构。