1.HasMap基础
2.解决hash冲突方法
3.jdk1.7和1.8的区别
1.7
-
头插法 -
hash()->4次扰动 -
先扩容再插入 -
数组+链表
1.8
-
尾插法 -
hash()->1次扰动 -
数组+链表/红黑树
-
先插入再扩容
4. jdk1.8HashMap插入原理
-
判断数组是否为空,为空进行初始化; -
不为空的,计算插入key的hash值,通过(n-1)&hash计算存放在数组的下标index; -
查看是否存在数据,没有数据就构造一个Node存放其中; -
存在数据,则证明发生了hash冲突(两个key的hash值一样),然后判断他的key是否相等,相等的话,用新值替换旧值 -
如果不相等,判断当前节点是否为树节点,如果是树形节点,插入红黑树中 -
如果不是树节点,插入链表中;然后判断链表长度是否大于8并且数组长度大于64,如果满足链表转换为红黑树 -
插入完成后判断节点数是否大于阈值,如果大于扩容为原来数组二倍
5.hashmap初始化
????????一般如果new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。
6.hashmap的扩容机制
当数组长度达到(最大程度*负载因子(0.75))的就会扩容数组。扩容大小为原数组的二倍。
扩容引子为什么为0.75:
-
负载因子过大,虽然空间利用率上去了,但是时间效率降低了(大量的hash冲突)。 -
负载因子太小,虽然时间效率提升了,但是空间利用率降低了 -
负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。
hashmap扩容为什么是2倍:
HashMap计算添加元素的位置时,使用的(hash运算)位运算,这是特别高效的运算;另外,HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!
位运算 :当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞
7.hashmap为什么1.8采用红黑树
????????链表的时间复杂度是O(n),红黑树的时间复杂度O(logn),很显然,红黑树的复杂度是优于链表的,红黑树拥有更高的查找性能
?
为什么不直接用红黑树:
????????因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。红黑树的空间复杂度比较大,只有节点足够多,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树。
为什么是大于8才扩容:
链表中节点数是8的概率已经接近千分之一,而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树,提高性能。
1.8链表红黑树节点转化机制
节点数大于8转为红黑树 节点数小于6转化为链表
红黑树特点
红黑树核心是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
-
节点是红色或黑色。 -
根节点是黑色。 -
所有叶子都是黑色。 -
从每个叶子到根的所有路径上不能有两个连续的红色节点 -
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
红黑树是平衡二叉树 平衡二叉树的特点:
-
所有节点最多拥有两个子节点,即度不大于2; -
左子树的键值小于根的键值,右子树的键值大于根的键值; -
任何节点的两个子树的高度最大差为1。
8.为什么1.8将头插法改为尾插法
因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
????????A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:
?
?
9.hashmap是线程安全的,怎么解决
????????不是,在多线程的情况下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,
如何解决:
-
java中有hashTable(哈希表),ConcurrentHashMap可以实现线程安全的Map。 -
HashTable(全段锁)是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大, -
ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。
10.hashTable
11.ConcurrentHashMap
????????Java7 中 ConcruuentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。
????????Java8 中的 ConcruuentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
12.ConcurrentHashMap的分段锁的实现原理
????????ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。
13.ConcurrentHashMap和HashTable有什么区别?能否取代HashMap?
答:主要区别体现在实现线程安全的方式上
-
在jdk1.7时,ConcurrentHashMap采用分段锁方式对整个桶数组进行了分割分段(Segment)(其中segment继承自ReentrantLock)0每一把锁只锁容器中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提供了并发访问率; 到了jdk1.8,直接摒弃了Segment的概念,直接采用Node数组+链表+红黑树来实现,并发控制使用synchronized和CAS来操作。 -
Hashtable则直接对整个结构加锁(synchronized)来保证线程安全,效率非常低下。当一个线程访问同步方法时,其它线程也访问同步方法,可能会进入阻塞或轮询状态,如果使用put添加元素,另一个线程就不能使用put添加元素,也不能使用get。 -
不能取代HashMap,Hashtable的任何操作都会把表锁住,是阻塞的,好处是总能够获取最实时的更新,ConcurrentHashMap为非阻塞的,在更新时会局部锁住某部分数据,但不会把整个表都锁住,同步读取操作是完全非阻塞的,在合理条件下效率非常高,坏处是在大量的读取操作时不能保证数据的实时更新。
14.HashMap是有序还是无序的,有序的map有哪些
hashmap是无序的,是根据hash值随机插入的\
有序的Map有那些
LinkedHashMap 和 TreeMap
15.LinkedHashMap
????????LinkedHashMap可以认为是HashMap+LinkedList,也就是说,它使用HashMap操作数据结构,也用LinkedList维护插入元素的先后顺序.
LinkedHashMap的特点
-
key和value都允许为空 -
key重复会覆盖,value可以重复 -
有序的 -
LinkedHashMap是非线程安全的
16.LinkedHashMap怎么实现有序的
????????LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
11.TreeMap怎么实现有序的
????????TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较。????????
|