map集合源码

172 阅读5分钟

HashMap源码

总结:key和value都允许null值,非线程安全,无法保证有序。

!!!jdk1.8之前底层为数组+链表

为什么有链表?

首先hashMap对于要存放的数据的话会进行hashCode运算,由于hashCode的值太大,会对hashCode的值&hashMap的长度 取余,这样就能得到对应的该值在第几个位置,而如果有多个相同的key存放到hashMap的话,通过链表关联起来,把新增的内容放在头部(如果放到尾部的话链表会进行迭代浪费性能),这样就可以获取具体的值了。

hashMap为什么默认是2的幂次方?

比如15的二级制是 0000 1111

随机hashCode值的二级制是1010 1010

取&操作后为 0000 1010

&的运算为:如果都是1则全部为1,否则为0

那么就是hashCode二级制的千数位的值,范围为0000-1111,也就是0-15

1.7的源码

` static int indexFor(int h, int length) {

// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

return h & (length-1);

}`

length-1也就是2的幂次方要-1才能得到奇数进行运算

1.7中hashMap在多线程的情况下进行扩容的话会出现循环链表的情况。

1.8后hashMap底层变成数组+链表+红黑树

** 为什么会引入红黑树?**

由于JDK1.7之前只是单纯的数组+链表结构,如果链表长度过大,那么在get元素的时候效率就会变得非常低,这是由链表的特性引起的,因此JDK1.8改成数组+链表+红黑树,如果链表元素个数大于8的话,就会形成一个红黑树,将超过8的元素放入至另外个节点中,提升查询效率。

此外,JDK1.8后插入元素的话是插入到链表的尾部!!!这是因为红黑树需要对链表的元素进行判断,如果超过8的话会将多余的元素放至到另外个节点,如果放到头部的话元素会进行移动就无法准确判断,此外,扩容的话也不会出现循环链表问题。

为什么链表超过8就会转成红黑树?

这是因为红黑树需要进行左旋右旋、变色操作,会耗费性能,jdk公司经过测试后发现在链表元素小于8的时候使用链表性能会更好,而大于8的时候红黑树比链表性能更优秀

hashMap默认长度为16,装载因子默认为0.75,那么初始容量就位16*0.75=12。

为什么会是16和0.75?

默认长度为16的原因:首先长度必须为2的幂次方,假设分配4、8,那么容量过小会容易导致扩容,浪费性能,而如果是32、64,容量过大又浪费资源。

默认装载因子0.75:是一种折中的办法,因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数。目的是提高空间利用率和减少查询成本的折中。

这2个数字的取值目的是为了让hash函数分布均匀。

hashMap的扩容方式为2N,即一次性扩容2倍。

hashMap为什么使用红黑树而不用其他数据结构?

如自平衡二叉树(AVL树):AVL树的话在查询的时候效率会更高,但是这是在牺牲新增、删除功能的前提下,而红黑树的话在查询、新增、删除的时候性能都会相对来说更加平衡。

ConcurrentHashMap

属于线程安全的map

JDK1.8之前采用分段锁机制,即segement[],其继承了ReentrantLock类,和普通的hashMap一样,都有默认长度和装载因子,但是多了个并发级别(DEFAULT_CONCURRENCY_LEVEL默认为16),这个的意思是如果长度为16,并发级别为8的话,就是把2个元素进行加锁,分成8段,简单的说并发级别就是这个ma想要进行分段的次数。

而1.8后取消了segement[]机制,改成CAS锁和synchronized

为什么要有这样的变动?

1、JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点) 2、jdk1.8的话Synchronized是将每一个Node对象作为了一个锁,这样就将锁细化了,那么出现并发的可能性就变得极低,其次,即使发生争抢锁的情况,只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销.

但如果是ReentrantLock呢?它则只有在线程没有抢到锁,然后新建Node节点后再尝试一次而已,不会自旋,而是直接被挂起,这样一来,我们就很容易会多出线程上下文开销的代价.当然,你也可以使用tryLock(),但是这样又出现了一个问题,你怎么知道tryLock的时间呢?在时间范围里还好,假如超过了呢?

HashTable源码:

底层为hash表结构,key和value均不能为空,属于线程安全。其中默认长度为11,装载因子默认为0.75,

扩容规则为:2*原数组长度+1,如 HashTable的容量为11,一次扩容后是容量为23。由于速度非常低,因此基本废弃掉。

LinkedHashMap源码

继承自hashMap,底层为双向链表+哈希表结构,按照先进先出(FIFO)原则,获取到的元素顺序是有序的。

TreeMap源码

底层采用红黑树结构,非线程安全,按照Comparable自然排序或者实现Comparator接口进行自定义排序,