HashMap<br><br>与 HashSet 对应,也是无序的,O(1)。<br>
<b>由数组和链表组合构成的数据结构,允许使用 null 值和 null 键</b>
我们都知道数组长度是有限的,在有限的长度里面我们使用哈希,哈希本身就存在概率性,就是”帅丙“和”丙帅“我们都去hash有一定的概率会一样,就像上面的情况我再次哈希”丙帅“极端情况也会hash到一个值上,那就形成了链表。
每一个节点都会保存自身的hash、key、value、以及下个节点
新的Entry节点在插入链表的时候,插入方式
jdk 8之前是<b>头插法</b>
<b>同一位置上新元素总会被放在链表的头部位置,会改变链表的上的顺序。</b><br>在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。<br><b>会造成无限循环的问题</b>
jdk8之后是<b>尾插法</b>
<b>在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。</b>
<b>扩容机制</b><br>数组容量是有限的,数据多次插入的,<br>到达一定的数量就会进行扩容,也就是resize。<br>
因素
Capacity:HashMap当前长度。<br><br>LoadFactor:负载因子,默认值0.75f。
扩容
分为两步<br><br>扩容:创建一个新的Entry空数组,长度是原数组的2倍。<br><br>ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组
<b>为什么要重新哈希?<br><br></b>因为长度扩大以后,Hash的规则也随之改变。<br><br>Hash的公式---> index = HashCode(Key) & (Length - 1)<br>
<b>初始化大小为16</b>
<b>为了位运算的方便,位与运算比算数计算的效率高了很多</b>
<b>实现均匀分布。</b><br><br>只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。<br>
<b>链表有红黑树</b>
二叉树
1.左子树上所有结点的值均小于或等于它的根结点的值。<br>2.右子树上所有结点的值均大于或等于它的根结点的值。<br>3.左、右子树也分别为二叉排序树。
<b>当二叉树多次插入新的节点,导致的不平衡,红黑树应用而生。</b>
1.节点是红色或黑色。<br><br>2.根节点是黑色。<br><br>3.每个叶子节点都是黑色的空节点(NIL节点)。<br><br>4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)<br><br>5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
<b>如何保证红黑树始终是红黑树</b>
变色
旋转
在极限状态下,HashMap在同一索引中放入第十一个元素时才会转变为红黑二叉树<br><br>那么由此我们会想为什么会这样呢?<b>Java中明明预设了阈值"8"</b>,怎么会到第十一才转为红黑二叉树呢?<br><br>其实在HashMap中存在扩容机制,<br>它一开始默认是长度为16的数组,<br>当某一个索引中的元素超过到8时,它优先会选择将长度扩容2倍(即长度为32)的数组,<br>它会认为是不是由于数组的长度不够才导致一个索引中元素过多;<br>但当它发现长度为32时,某一个索引中的元素还是超过9时,<br>它还是会优先选择将长度再次扩容到2倍(即长度为64)的数组,<br>在长度为64的数组中,它发现某一个索引中的元素还是超过10时,它就会对该索引所在的链表转化为红黑二叉树.<br>所以在第十一个元素时,就会对该索引所在的链表转化为红黑二叉树.
<b>为啥我们重写equals方法的时候<br>需要重写hashCode方法呢?</b><br>
<b>在java中,所有的对象都是继承于Object类。Object类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的</b>
<b>在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样<br><br>对于值对象,==比较的是两个对象的值<br><br>对于引用对象,比较的是两个对象的地址</b>
大家是否还记得我说的HashMap是通过key的hashCode去寻找index的,那index一样就形成链表了,也就是说”帅丙“和”丙帅“的index都可能是2,在一个链表上的。<br><br>我们去get的时候,他就是根据key去hash然后计算出index,找到了2,那我怎么找到具体的”帅丙“还是”丙帅“呢?<br><br>equals!是的,所以如果我们对equals方法进行了重写,建议一定要对hashCode方法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。
线程不安全,线程安全需使用
HashTable
适合在多线程的情况下使用,但是效率可不太乐观。<br>对数据操作的时候都会上锁,所以效率比较低下。<br>
CurrentHashMap
Collections.synchronizedMap(Map)
两个构造器<br>如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象。<br><br>如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象<br>
<b>Hashtable 跟HashMap不一样</b>
<b>Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。</b><br><br>Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理。<br>
<b>Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。</b>
<b>实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。</b>
<b>初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。</b>
<b>扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。</b>
<b>迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。</b>
总结:当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。
<b>快速失败</b>(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,<br>如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),<br>则会抛出Concurrent Modification Exception。<br>
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。<br><br>集合在被遍历期间如果内容发生变化,就会改变modCount的值。<br><br>每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。<br><br>Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。<br><br>因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
LinkedHashMap<br>这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。<br>
ConcurrentHashMap
<b>是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。</b>
HashEntry跟HashMap差不多的,但是不同点是,<br><b>使用volatile去修饰了他的数据Value还有下一个节点next。</b><br>
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)<br><br>禁止进行指令重排序。(实现有序性)<br><br>volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性
<b>并发度高的原因</b>
<b>jdk1.7</b><br><br>ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。<br><br>每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。<br>
j<b>dk1.8<br><br></b>其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。<br><br>跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)<b><br></b><br>
<b>jdk1.8 put操作</b>
ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:<br><br>根据 key 计算出 hashcode 。<br><br>判断是否需要进行初始化。<br><br>即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。<br><br>如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。<br><br>如果都不满足,则利用 synchronized 锁写入数据。<br><br>如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
<b>CAS</b>
CAS 是乐观锁的一种实现方式,是一种轻量级锁,<br>JUC 中很多工具类的实现就是基于 CAS 的。<br><br>线程在读取数据时不进行加锁,在准备写回数据时,<br>比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。<br>
<b>ABA 问题</b>
一个线程把值改回了B,又来了一个线程把值又改回了A,<br>对于这个时候判断的线程,就发现他的值还是A,<br>所以他就不知道这个值到底有没有被人改过<br>
(1) 版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,<br>每次判断就连值和版本号一起判断,判断成功就给版本号加1。<br><br>(2) 时间戳也可以,查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间
<b>synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?</b>
synchronized之前一直都是重量级的锁,但是后来官方是对他进行过升级的,<br>他现在采用的是锁升级的方式去做的。<br><br>针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,<br>就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,<br>如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁<br>
<b>Jdk1.8 get 操作</b>
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。<br><br>如果是红黑树那就按照树的方式获取值。<br><br>就不满足那就按照链表的方式遍历获取值。