HashMap是一种常用的数据结构,它主要通过固定值(key)获取内容的场景,时间复杂度最快可以优化到O(1),当然效果不好的时候复杂度是O(logN),或者O(N),虽然固定值的查找提高了速度,但是HashMap不能保证固定值,也就是key的顺序,于是这时候HashMap就出现了,它的查找、删除、更新的时间复杂度都是O(lonN),但是它可以保证key的有序性。
HashMap采用的是数组和哈希的方式实现,巧妙的通过key的哈希路由到每一个数组用于存放内容,这时候通过key获取value的时间复杂度就是O(1),当然,由于key的哈希可能冲突,所以需要针对冲突的时候做处理,HashMap的每一个数组里面存储的实际上都是一个链表,key的哈希冲突以后会追加到链表上面,这时候再通过key获取value值的时候时间复杂度就变成了O(n),那么查询一样会比较慢,为了优化,HashMap当一个key的冲突次数超过TREEIFY THRESHOLD的时候就会把链表转化为红黑树,这样虽然插入的时候增加了时间复杂度,但是对于查询效率有很大的提高。
HashMap不是线程安全的,因为他的底层get和put方法均没有被synchronized和引用锁机制,但是可以利用Collections.sychronizedMap()方法转换成线程安全的。
红黑树
红黑树的每个节点都是红色和黑色两种颜色,他的特性是:
1,根节点是黑色的
2,红色节点的子节点必须是黑色
3,任意一条路径的黑色节点个数相同。
二叉搜索树
左子树的值小于根节点,右子树的值大于根节点。
数组扩容为何要是2的指数?
数组默认的的容量大小是16,当然是可以设置的,但是设置的数必定会强制向上转化为2的指数的大小。
其主要原因是在于在我们进行插入数据的时候,数据插入的过程是这样的:
1,首先把我们的键值通过hashCode方法转换成一个hash值。
2,然后对hash值进行取余运算,得到我们数组的索引值
3,进入数组所指向的链表进行插入数据(其中jdk1.7采用Entry结点的头插法,jdk1.8以后采用Node结点的尾插法)。
我们初始容量并且扩容一定要是2的指数的主要原因是第2步,在取余数的运算中,如果我们的容量不是2的指数,只能进行取模运算,这个时候的运算量是比较大的,但是如果是2的指数,我们完全可以采用位运算,这两种运算方法通过分析可以知道取模运算的时间约等于4-5倍的位运算。
因此扩容为2的指数是一个能够极大提高时间运行效率的方法。
jdk1.7线程不安全的链表循环详解
jdk1.7采用头插法进行插入数据的,它的线程不安全主要体现在产生死锁和数据丢失两个方面的,死锁的原因是在两个线程进行并发扩容的时候导致产生链循环。
在jdk1.8解决了这个问题,但是对于数据丢失通过其他方面进行解决的。
加载因子为什么是0.75?
加载因子主要跟数组的扩容有关,当数组填充的长度大于数组长度*加载因子时会发生扩容,它的loadfactory的初始值是0.75。
它的初始值这样设置主要是考虑到性能的两个方面,一个是时间效率,一个是空间利用率。
我们通过极端情况进行分析:
首先,如果加载因子时1,这个时候只有所有的数组都被填充完毕才会扩容,这样可能会导致产生大量的链表和红黑树,极大的影响查询效率。
另一方面,如果加载因子时0.5,这个时候在hash表即使得到了最大限度的利用,它的空间利用率也仅仅50%,空间利用率明显过低。
因此,我们取加载因子为0.75,是一个比较合适的数值,但是并不是不可更改的,在一些文献上通过牛顿二项式算出加载因子最合适的值是0.693,为了方便计算,可以将数值变为0.75。
链表转红黑树需要注意的地方
首先如果数组长度小于64的时候,优先考虑数组扩容;
然后,如果数组长度大于等于64,并且链表长度已经大于TREEIFY-THRESHOLD的时候,我们才会将数组转化为红黑树,需要注意的是,TREEIFY-THRESHOLD默认数值是8,在当前链表长度为9的时候才会转化为红黑树。
concurrentHashmap
jdk1.7之前ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
jdk1.8之后采用CAS+sychnorized方式加锁
HashMap-红黑树
于 2020-04-04 16:01:07 首次发布