HashMap和ConcurrentHashMap

10 篇文章 0 订阅

HashMap
HashMap是无序且不安全的 数据结构
HashMap是以key-value 键值对的形式存储的,key值是唯一的,可以为null,一个key只能对应一个value,value可以重复
HashMap提供了get方法,可以通过key值取对应的value值,
jdk8前HashMap
底层是数组加链表,链表新节点插入顺序是插入头结点,
jdk8以后HashMap
底层数组加链表加红黑树,当链表长度大于等于8的时候链表会变成红黑树
链表新节点插入顺序是插入尾结点
hash算法简化
resize逻辑修改,8以前会出现死循环,8以后不会

HashMap的容量和扩容机制
默认的负载因子是0.75 ,负载因子的作用是计算扩容阈值的,
默认创建的无参HashMap的初始长度默认是16,阈值=HashMap的长度*0.75,当当前长度大于等于阈值的时候HashMap就会进行自动扩容,
扩容的规则是 容量转为二进制再向左移动一位得到的,因为table数组的长度必须是2的次方数,扩容其实每次都是按照上一次的tablesize位运算得到的二进制数做一次左移1位运算。
这样看,容量始终是扩容一倍,为什么不直接乘2呢
因为cpu不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,效率比较低,如果用位运算的话,cpu就比较高效

HashMap的存储原理
1.获取到传过来的key,调用hash算法获取到hash值
2.获取到hash值之后调用indexFor方法,通过获取到的hash值以及数组的长度算出数组的下标(把哈希值和数组容量转换为二进制,再在数组容量范围内与哈希值进行一次原酸,同为1则1,不然则为0,得出数组的下标值,这样可以保证计算出的数组下标不会大于当前数组容量)
3.把传过来的key和value存到该数组下标中
4.如果该数组下标下已经有值了,则使用链表,jdk7是把新增元素添加到头部节点,jdk8则添加到尾部节点


HashMap是线程不安全的
在多线程的情况下,使用HashMap进行put操作会引起死循环,所以在多线程的情况下不能使用HashMap
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。也就是说对于Hashtable而言,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占。相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。
ConcurrentHashMap是线程安全的,效率也要比hashtable效率高,
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
ConcurrentHashMap是如何保证并发安全的?
JDK7中ConcurrentHashMap是通过ReentrantLock+CAS+分段思想来保证的并发安全的,ConcurrentHashMap的put方法会通过CAS的方式,把一个Segment对象存到Segment数组中,一个Segment内部存在一个HashEntry数组,相当于分段的HashMap,Segment继承了ReentrantLock,每段put开始会加锁。

在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值,所得的和就是所有的元素个数。
 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值