常见的有数据结构有三种结构:数组结构、链表结构、哈希表结构;
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低;链表是为了解决哈希冲突而存在内部解决方案(拉链法);
而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间;链表长度大于8时转化为红黑树,小于6时转化为链表;
首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率;当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中;
如果线程T1和线程T2同时得到了一个信息:这个索引处A.next=null,那么两个线程都会执行A.next=(K,V)这么一步,两个线程同时put的时候,会导致一个元素的丢失;
put和get并发时,可能导致get为null:如果线程T1put一个元素,发现需要resize,table重新创建时,是一个空的数组,此时如果其他线程使用get()时,会得到null;
增删是在链表上完成的,而查询只需扫描部分,则效率高;
红黑树是一种近似平衡的二叉查找树,其主要的优点就是“平衡“,即左右子树高度几乎一致,以此来防止树退化为链表,好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢;
①每个节点非红即黑
②根节点总是黑色的
③如果节点是红色,则它的子节点必须是黑色(反之,不一定)
④每个叶子节点都是黑色的空节点(NIL节点)
⑤从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(相同的黑色高度)
HashMap 的添加、获取时需要通过 key 的 hashCode() 进行 hash(),然后计算下标 ( n-1 & hash),从而获得要找的同的位置。当发生冲突(碰撞)时,利用 key.equals() 方法去链表或树中去查找对应的节点;
h = key.hashCode()) ^ (h >>> 16), hashCode 进行无符号右移 16 位,然后进行按位异或,得到这个键的哈希值,由于哈希表的容量都是 2 的 N 次方,在当前元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或;
如果key为null会放在第一个bucket(即下标0)位置, 而且是在链表最前面(即第一个位置);
Map m = Collections.synchronizeMap(hashMap);工具类Collections中的synchronizedMap方法,这个方法返回一个SynchronizedMap,其在方法上,全部加上synchronized,类似于HashTable;
JDk7 通过 hashcode&(Length-1)后的一系列异或运算计算出数组的 index:
如果这个 index 位没有元素则直接占位;只有一个元素时,开始比较 key 是否是同一个对象,如果是同一对象则覆盖,否则把当前 entry.next 指向原来 entry,让其退位让贤,称为头插;问题来了,当这个位置有 n 个 entry 时,则要遍历出每个 entry,然后去比较当前 key 与遍历出来的是否是同一个 key,是则接覆盖并退出循环,否则进行下一次遍历,都没有同样的 key 会遍历完整个 entry 链。既然都要遍历完整个 entry,为什么不直接在尾部插入呢,用尾插法不行么?其实很简单,头插法大多数情况是方便第一和第二种情况的,因为散列性好的话 hash 冲突的概率自然比较小,没有元素或只有一个元素,遍历个锤子,减少遍厉次数呗。而且既然第一和第二种情况时已经写了一个头插方法,就没必要再写一个尾插方法了吧。Jdk8 使用尾插法,因为 jdk8 里的单向链表变成红黑树时的条件是确定的,node 链长度+1 大于等于 8 且容量大于 64 时扩容,遍历几次而已,那就可以放心遍历啦,于是就用了尾插法,为了写代码方便。头插法在并发扩容时,会形成死环,这并不是改成尾插法的主要原因,因为在并发情况使用 hashmap 本身是错误的用法;
链表头插法的会颠倒原来一个散列桶里面链表的顺序(逆序)。并发的时候原来的顺序被另外一个线程a颠倒,而被挂起线程b恢复后拿扩容前的节点和顺序继续完成第一次循环后,又遵循a线程扩容后的链表顺序重新排列链表中的顺序,最终形成了环。
调用put不一定是新增数据,还可能是覆盖掉原来的数据,这里就存在了一个key的比较问题。以先扩容为例,先比较是否是新增的数据,再比较是否需要增加数据后扩容,这样比较会浪费时间,而后扩容,就在中途直接通过return返回了,根本执行不到是否扩容,这样可以提高效率的。
如果是0.5,临界值是8 则很容易就触发扩容,而且还有一半容量还没用;如果是1,当空间被占满时候才扩容,增加插入数据的时间;0.75即3/4,capacity值是2的幂,相乘得到结果是整数;
两个线程同时触发 resize,中间过程有可能会造成元素指向死循环;
因为红黑树相对于链表维护成本大,红黑树在插入新数据之后,可能会通过左旋、右旋、变色来保持平衡,造成维护成本过高,故链路较短时,不适合用红黑树;
二叉树在极端情况下会退化成链表,而红黑树是有balance不会退化成链表;时间复杂度:链表O(n/2),红黑树O(log(n));
为了减少hash冲突,就是为了让数据均匀分布。此时我们一般使用公式(hashCode%size),这样可以达到最大的平均分配。而(n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n;&运算速度快,比%取模运算块,根据计算,Java的%、/操作比&慢10倍左右;能保证 索引值 肯定在 capacity 中,不会超出数组长度;