1.HashMap的数据结构:
JDK1.8:数组+链表/红黑树 数据结构原理:链表长度>8&数组大小>=64=》转为红黑树存储;红黑树节点个数<6转为链表。tips:为什么不是小于8是为了防止频繁的结构转换增加开销。那为啥是8呢?发生hash碰撞的几率8次的概率为百万分之6,够了。HashMap的数据插入原理:
重点说明:上面计算数组位置的方法是:通过 (n - 1) & hash计算应当存放在数组中的下标 index ;叫做位运算为啥(n-1)更接近于取模,为啥采用位运算是因为效率高,不存在二进制和十进制的转换。这里在补充一点HashMap初始化数组大小的问题,HashMap数组初始化大小为16,为啥呢?理论上来说2的整数次幂都可以,但是如果是2,4或者8就会有点小,添加不了多少数据就会扩容,影响性能,如果是32或者更大,就会浪费空间。所以16是一个经验值保留下来的。
2.HashMap的hash函数设计:
在HashMap中,首先是通过key的hashcode(32位的int值)然后让hashcode的高16位和低16位进行异或操作。为啥这样设计:1.上面的hash函数也叫扰动函数,主要考虑尽可能降低hash碰撞,越分散越好。
算法一定要尽可能的高效,因为这是高频操作,因此采用位运算。
因为hashcode的范围是int类型,大概40亿的映射空间,如果只是hashcode,很少会出现碰撞,但是数组和内存是发放不下的,初始大小才16的数组只能进行取模运算/位运算来达到目的。
HashMap的数组长度要取2的整数幂,这样数组长度-1刚好"低位掩码",加上hash函数(扰动函数)降低碰撞。
3.Java1.8的Java1.7对比:
3.1 扰动的次数减少:
java1.8跟java1.7在这里的区别就hash函数里面是java8只扰动了一次,为了效率。java7在这里扰动了四次。
3.2 结构变化:
7里面的数组+链表===》数组+链表或红黑树。为什么这样做,前者的查询效率是n后者的查询效率是log2(n),所有当链表数据量大的时候会有效率问题。
3.3 插入变化:
链表的插入方式从头插法改成了尾插法,简单来说就是1.7中是往前面插入,1.8中是往后插入,这样做的目的是为了防止扩容的时候死循环。
3.4 扩容的变化:
扩容的时候1.7是重新hash定位新数组的位置,1.8则是采用更简单的逻辑判断,位置不变或索引+旧容量大小。为什么1.8能这样,计算数组的位置的掩码仅仅只是高位多了一个1。
3.5 扩容的时间变化:
在插入时,1.7先判断是否需要扩容再插入,1.8是插入以后再判断是不是需要扩容
4.HashTable和Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map原理:
我们都知道的是HashMap是线程不安全的,1.7版本的时候会产生死循环、数据丢失、数据覆盖的问题,1.8中解决了其中的两个问题,仍然会有数据覆盖的问题,就是多线程并发操作下的值覆盖。如果业务场景中需要线程安全,就要使用线程安全的Map类,一般我们使用的是ConcurrentHashMap。
ConcurrentHashMap:使用的分段锁,降低锁粒度,提高并发度和效率。1.8相对1.7也是提升了效率,成员变量之间使用了volatile修饰,避免了指令重排,保持内存可见。采用的CAS和synchronized结合实现赋值操作,只会锁住当前操作索引节点。
HashTable:直接在操作方法上synchronized关键字,锁住整个数组,效率会很低。SynchronizedMap:内部实现的对象锁实现。效率比HashTable会高点。
5.Map集合的顺序:
HashMap里面是无序的,所以只能循环遍历。LinkedHashMap:有序map,内部维护了一个单链表。单链表的before和after使其具有了有序的特性。TreeMap:内部通过Comprator比较器实现了排序。
总结
我上面的论述也是参考的网上我认为不错的针对HashMap的讲解,然后加入了我自己的理解,让大家能够更好的理解其中的原理内容,如果你有其他问题,欢迎关注我的公众号:Java时间屋 进行讨论。