HashMap、LinkedHashMap、HashTable、ConcurrentHashMap、TreeMap原理和区别
继承关系
区别
各种Map的实现原理
HashMap
原理
1. 工作原理
- JDK8以前【数组+链表】实现
使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低
(链表是主要为了解决哈希冲突而存在的——链地址法解决Hash冲突) - JDK8以后【数组+链表+红黑树】实现
如果哈希表单向链表中元素超过8个,那么单向链表这种数据结构会变成红黑树数据结构。当红黑树上的节点数量小于6个,会重新把红黑树变成单向链表数据结构。
2. map.put(k,v)原理
1)将k v封装到Node(节点)对象中
2)调用k 的hashCode()方法得出hash值
3)通过哈希算法,将hash值转换成数组的下标,如果下标位置没有元素,就把node添加到这个位置上。如果有,就以链表的形式存放,此时会拿着k和链表上的每个节点的k进行equal,如果返回的都是false,将其添加到链表末尾,如果某个返回了true,此节点的value会被覆盖。
【先通过key算出hash,然后用hash算出应该存储在table中的index,然后遍历table[index],看是否有相同的key存在,存在,则更新value;不存在则插入到table[index]单向链表的表头,时间复杂度为O(n)】
3. map.get(k)原理
1)调用k的hashcode计算hash值 通过哈希算法转换成数组的下标
2)利用下标定位,没有链表,返回此位置上的value或者null,有链表,拿着k和链表上的每一个节点进行equals,如果返回的都是null,则get返回的是null。否则,返回对应节点的value。
【通过key算出hash,然后用hash算出应该存储在table中的index,然后遍历table[index],然后比对key,找到相同的key,则取出其value,时间复杂度为O(n)】
HashMap面试题
1.扩容机制
- 当hashmap中的元素个数超过数组(16)大小*0.75(负载因子)时,就会进行数组扩容。
- HashMap的容量永远为2的幂次方,有利于哈希表的散列
- 在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
2.HashMap中的“死锁”是怎么回事?
HashMap是非线程安全,死锁一般都是产生于并发情况下。
我们假设有二个进程T1、T2,HashMap容量为2,T1线程放入key A、B、C、D、E。
在T1线程中A、B、C Hash值相同,于是形成一个链接,假设为A->C->B,
而D、E Hash值不同,于是容量不足,需要新建一个更大尺寸的hash表,
然后把数据从老的Hash表中 迁移到新的Hash表中(refresh)。
这时T2进程闯进来了,T1暂时挂起,T2进程也准备放入新的key,这时也 发现容量不足,也refresh一把。
refresh之后原来的链表结构假设为C->A,之后T1进程继续执行,链接结构 为A->C,这时就形成A.next=B,B.next=A的环形链表。
一旦取值进入这个环形链表就会陷入死循环。
3.HashMap中能put两个相同key吗?HashMap中的键值可以为null吗?
可以put相同key,但是后面的会替换前面的,最后仅保留一个,因为key唯一。
HashMap可以存储一个Key为null,多个会覆盖,可以有多个value为null的元素(Hashtable不可以存储key为null)
【 hashMap的key和value都可以为null,hashTable和concrrentHashMap却都不能。
因为单线程中,map可以通过map.contains(key)判断null是他的值还是未找到key。当多线程使用map.get(key)时返回为null,无法判断key是不存在还是值为空,多线程中,可能再两次调用间已经发生改变。】
4.为何随机增删、查询效率都很高?
增删是在链表上完成的,而查询时数组定位,只需扫描一部分链表,则效率高
5. HashMap插入元素时间复杂度
HashMap插入元素的时间复杂度 :
- 理想状态O(1)
- 如果只用到链表O(n)
- 如果用到了红黑树O(logn)
LinkedHashMap
LinkedHashMap是HashMap的子类,可以保持插入顺序。
通过双联表的结构来维护节点的顺序。
ConcurrentHashMap
java7底层通过Segment(ReentrantLock) +分段锁实现,一个Segment负责一组数据;
java8通过cas(compare and swap)来实现线程安全
CAS简介
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行
ConcurrentHashMapJDK1.7和JDK1.8区别
》1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
》2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
》3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁。
》4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
》5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
TreeMap
红黑树实现。
TreeMap是可以自动排序的,默认情况下comparator为null,这个时候按照key的自然顺序进行排,想自定义排序规则可以重写compare方法
HashTable
HashTable继承Dictionary类,实现Map接口。其中Dictionary类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类