DK8中的HashMap与JDK7的HashMap有什么不一样?
- JDK8中新增了红黑树,JDK8是通过数组+链表+红黑树来实现的
- JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
(1.7头插法实际上是插入效率更高,但是他会形成圆环,,增加了碰撞的几率,所以8后改成了尾插法,虽然插入效率上有所降低,但是可以忽略不记,而且8后也采取了红黑树,插入和查询效率有所提高,,) - JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否查过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值。
- JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容
为什么8使用红黑树,而不是其他树比如AVL?
原因:实际上ConcurrentHashMap采用的是读写锁,,就是说写的时候需要等待,,所以需要插入的效率高些,,正好红黑树的插入效率要高于AVL,所以就采用红黑树。
HashMap中PUT方法的流程?
- 通过key计算出一个hashcode
- 通过hashcode与“与操作”计算出一个数组下标
- 在把put进来的key,value封装为一个entry对象
- 判断数组下标对应的位置,是不是空,如果是空则把entry直接
- 存在该数组位置
- 如果该下标对应的位置不为空,则需要把entry插入到链表中
- 并且还需要判断该链表中是否存在相同的key,如果存在,则更新value
- 如果是JDK7,则使用头插法
- 如果是JDK8,则会遍历链表,并且在遍历链表的过程中,统计当前链表的元素个数,如果超过8个,则先把链表转变为红黑树,并且把元素插入到红黑树中
JDK8中链表转变为红黑树的条件?
- 链表中的元素的个数为8个或超过8个
- 同时,还要满足当前数组的长度大于或等于64才会把链表转变为红黑树。为什么?因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解决链表过长的问题。
HashMap扩容流程是怎样的?
- HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新数组上来,这样才是数组的扩容
- 在HashMap中也是一样,先新建一个2倍数组大小的数组
- 然后遍历老数组上的每一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去
- 在这个过程中就需要遍历链表,当然jdk7,和jdk8在这个实现时是有不一样的,jdk7就是简单的遍历链表上的每一个元素,然后按每个元素的hashcode结合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率
- 而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置。
- 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收到。
为什么HashMap的数组的大小是2的幂次方数?
JDK7的HashMap是数组+链表实现的
JDK8的HashMap是数组+链表+红黑树实现的
当某个key-value对需要存储到数组中时,需要先生成一个数组下标index,并且这个index不能越界。
在HashMap中,先得到key的hashcode,hashcode是一个数字,然后通过 hashcode & (table.length - 1) 运算得到一个数组下标index,是通过与运算计算出来一个数组下标的,而不是通过取余,与运算相比于取余运算速度更快,但是也有一个前提条件,就是数组的长度得是一个2的幂次方数。
JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?
- JDK8中新增了红黑树
- JDK7中使用的是头插法,JDK8中使用的是尾插法
- JDK7中使用了分段锁,而JDK8中没有使用分段锁了
- JDK7中使用了ReentrantLock,JDK8中没有使用ReentrantLock了,而使用了Synchronized
- JDK7中的扩容是每个Segment内部进行扩容,不会影响其他Segment,而JDK8中的扩容和HashMap的扩容类似,只不过支持了多线程扩容,并且保证了线程安全
ConcurrentHashMap是如何保证并发安全的?
JDK7中ConcurrentHashMap是通过ReentrantLock+CAS+分段思想来保证的并发安全的,在JDK7的ConcurrentHashMap中,首先有一个Segment数组,存的是Segment对象,Segment相当于一个小HashMap,Segment内部有一个HashEntry的数组,也有扩容的阈值,同时Segment继承了ReentrantLock类,同时在Segment中还提供了put,get等方法,比如Segment的put方法在一开始就会去加锁,加到锁之后才会把key,value存到Segment中去,然后释放锁。
同时在ConcurrentHashMap的put方法中,会通过CAS的方式把一个Segment对象存到Segment数组的某个位置中。
同时因为一个Segment内部存在一个HashEntry数组,所以和HashMap对比来看,相当于分段了,每段里面是一个小的HashMap,每段公用一把锁,同时在ConcurrentHashMap的构造方法中是可以设置分段的数量的,叫做并发级别concurrencyLevel.
JDK8中ConcurrentHashMap是通过synchronized+cas来实现了。在JDK8中只有一个数组,就是Node数组,Node就是key,value,hashcode封装出来的对象,和HashMap中的Entry一样,在JDK8中通过对Node数组的某个index位置的元素进行同步,达到该index位置的并发安全。同时内部也利用了CAS对数组的某个位置进行并发安全的赋值。
JDK8中的ConcurrentHashMap为什么使用synchronized来进行加锁?
JDK8中使用synchronized加锁时,是对链表头结点和红黑树根结点来加锁的,而ConcurrentHashMap会保证,数组中某个位置的元素一定是链表的头结点或红黑树的根结点,所以JDK8中的ConcurrentHashMap在对某个桶进行并发安全控制时,只需要使用synchronized对当前那个位置的数组上的元素进行加锁即可,对于每个桶,只有获取到了第一个元素上的锁,才能操作这个桶,不管这个桶是一个链表还是红黑树。
想比于JDK7中使用ReentrantLock来加锁,因为JDK7中使用了分段锁,所以对于一个ConcurrentHashMap对象而言,分了几段就得有几个ReentrantLock对象,表示得有对应的几把锁。
而JDK8中使用synchronized关键字来加锁就会更节省内存,并且jdk也已经对synchronized的底层工作机制进行了优化,效率更好。
JDK7中的ConcurrentHashMap是如何扩容的?
JDK7中的ConcurrentHashMap和JDK7的HashMap的扩容是不太一样的,首先JDK7中也是支持多线程扩容的,原因是,JDK7中的ConcurrentHashMap分段了,每一段叫做Segment对象,每个Segment对象相当于一个HashMap,分段之后,对于ConcurrentHashMap而言,能同时支持多个线程进行操作,前提是这些操作的是不同的Segment,而ConcurrentHashMap中的扩容是仅限于本Segment,也就是对应的小型HashMap进行扩容,所以是可以多线程扩容的。
每个Segment内部的扩容逻辑和HashMap中一样。
JDK8中的ConcurrentHashMap是如何扩容的?
首先,JDK8中是支持多线程扩容的,JDK8中的ConcurrentHashMap不再是分段,或者可以理解为每个桶为一段,在需要扩容时,首先会生成一个双倍大小的数组,生成完数组后,线程就会开始转移元素,在扩容的过程中,如果有其他线程在put,那么这个put线程会帮助去进行元素的转移,虽然叫转移,但是其实是基于原数组上的Node信息去生成一个新的Node的,也就是原数组上的Node不会消失,因为在扩容的过程中,如果有其他线程在get也是可以的。
JDK8中的ConcurrentHashMap有一个CounterCell,你是如何理解的?
CounterCell是JDK8中用来统计ConcurrentHashMap中所有元素个数的,在统计ConcurentHashMap时,不能直接对ConcurrentHashMap对象进行加锁然后再去统计,因为这样会影响ConcurrentHashMap的put等操作的效率,在JDK8的实现中使用了CounterCell+baseCount来辅助进行统计,baseCount是ConcurrentHashMap中的一个属性,某个线程在调用ConcurrentHashMap对象的put操作时,会先通过CAS去修改baseCount的值,如果CAS修改成功,就计数成功,如果CAS修改失败,则会从CounterCell数组中随机选出一个CounterCell对象,然后利用CAS去修改CounterCell对象中的值,因为存在CounterCell数组,所以,当某个线程想要计数时,先尝试通过CAS去修改baseCount的值,如果没有修改成功,则从CounterCell数组中随机取出来一个CounterCell对象进行CAS计数,这样在计数时提高了效率。
所以ConcurrentHashMap在统计元素个数时,就是baseCount加上所有CountCeller中的value值,所得的和就是所有的元素个数。