ConcurrentHashMap内层原理

集合包装

  1. HashMap
    • 实现原理:
      • JDK1.8之前:
        • 结构:采用的是hashEntry数组加链表的结构,首先计算当前存入的k-v的key的hashcode值来查看应该被放到数组的那里,找到对应的索引后再用key去比对对应索引中的链表,如果有key值与当前的值是相等的则进行覆盖,否则将该k-v加入到链表中。但是当hash冲突过多时就会导致链表长度越来越长,那HashMap的查询效率也会降低。
        • 插入方式:采用头部插入好处是刚刚put进入的值可能会马上用到,这样不需要遍历比较到链表尾部。而在在扩容后进行重排的时候也不用在遍历到链表尾部进行插入;但是如果线程A在扩容的过程中,另外一个线程B进行put操作时,在出现hash冲突,也需要进行扩容,所以对hashmap进行了rehash改变了链表的序列,之后再交由线程A时线程进行复制时因为next指向已经改变在进行复制到新的数组时就会出现闭环链
        • 扩容:hashMap的扩容是当加入新的Hashentry对象时会导致当前的数组占用长度大于数组长度*负载因子的容量时,这时会对HashMap进行扩容,默认负载因子为0.75;而hashmap的扩容则会生成一个原数组长度的两倍的HashEntry数组。之后对原数组中的元素进行rehash将原数组中的元素迁移到新的HashEntry数组中,最后将hashmap的数组引用指向新的HashEntry数组,至此完成hashmap的扩容过程,最后在将Hashentry对象put。这样在hashmap每一次扩容时都会极大的浪费资源。
      • JDK1.8:
        • 结构:相比于之前jdk1.8使用Node数组+链表+红黑树的结构,在当链表长度大于8时会将链表转换为红黑二叉树,红黑二叉数使根节点到达每个末节点的距离都相同。那为什么会在链表长度大于8时转换为红黑二叉树呢,因为当长度等于8时链表遍历速度为8/2=4,而转换为树形结构后根节点距末节点最远距离为3,而在此之前红黑二叉树的查询效率也可能比链表速度快,但是在链表转换二叉树的过程会耗费资源,所以选择在等于8是对链表进行树化。
        • 插入方式:采用尾部插入,以此来避免产生倒序链和循环链。
        • 扩容:jdk1.8也是在node数组插入新的Node对象后占用长度大于数组长度*负载因子的容量时进行扩容,且负载因子依然是0.75,并且也是扩容到原来的两倍。但是jdk1.8后不对键进行rehash,通过建立新的Node数组,直接判断其高位是否为1如果为1的话其索引值就会发生改变,具体原因是因为其索引计算公式为:index=hash值&(n-1)+index;n为数组长度,再用位运算符&进行计算后得知其高位是否增加1,如果增加其索引位置为原索引位置加上原数组长度,通过以上方式将原Node数组的元素进行迁移减少资源的开销。
      • 为什么hashMap的数组长度都必须为2的指数呢,因为在做索引计算时会引用公式index=hash值%2n=hash值&(2n-1),当n为2的指数时以上公式才得以成立,才能最大化的减小hash冲突,使对象能供均匀分布在hashmap的各个hashentry/node数组中,如果在初始化时自定义的大小非2的指数时,hashmap会默认通过tableSizeFor方法将其变更为2的指数。
  2. List
  3. Set
  4. 使用Conllections.synchronizedMap/List/Set 可以使其称为线程安全的集合但是这种方式只适合并发量比较小的情况,因为其内部只是通过增加 synchronized关键字对读写操作都加上了锁,使之方法之间都是串行结构,实际上可以理解为单线程处理,效率偏低。

ConcurrentHashMap

  1. JDK1.8之前:
    • 实现原理: CocurrentHashMap采用分段锁的方式使其达到线程安全,其内部是多个片段(Segment)组成,每个片段都包含了若干个HashEntry,可以理解为其实每个片段里面包含的就是一个hashMap。因为ConcurrentHahsMap分为了若干个片段,片段Segment继承自ReentrantLock,这样其本身就有了锁的功能,在线程对其操作的时候只用找到对应的片段进行加锁,保证其线程的安全。
      在这里插入图片描述
    • put方法:在对某个片段内进行put操作时,会首先通过tryLock()尝试获取锁,如果获取成功则进入方法进行执行put操作,如果没有获取到锁则会进入scanAndLockForPut()中尝试获取锁,多处理器情况下最大尝试次数为64次,单处理器尝试次数为1,一旦超过了最大尝试次数,会直接对该线程进行挂起,知道等待获取到锁的线程执行完毕并调用unlock()释放锁后,唤醒当前线程。而这个等待过程称为自旋等待。
    • get方法:默认采用不加锁的方式进行获取对应的值,如果获取的值为null的话,会加锁后重新获取一次值后返回。
    • size方法:concurrenthashMap首先会采用不加锁的方式获取Segment数组的长度,如果连续两次拿到的值都相等的话就直接返回。否则会对每个片段进行加锁,计算对应的size后再全部释放锁。
  2. JDK1.8:
    • 实现原理:1.8 直接抛弃了分段锁的使用,采用Node+链表+红黑树+CAS+sysnchroized的方式来保证其并发安全,在Node类中对next和value属性都设置了volatile同步锁。
      在这里插入图片描述
    • put方法:在key、value不为空的情况下,首先计算存入的key的hash值计算对应的索引,如果当前table还没有初始化则进行初始化,之后通过索引获取对应的节点,如果节点为空则通过cas操作尝试将key、value放入其中,若不行则判断当前节点的hash是否为-1,则进行迁移操作。否则对节点进行加锁,之后判断当前节点是链表还是红黑树,然后进行修改或增加node操作。与以往的分段锁不同的是,通过使用锁分离思想,只是将一个node锁住,而之前的操作都是无锁且线程安全的。
    • get方法:通过key相同以及hash值相同,对节点进行查找并获取。
    • size方法:1.8中不在通过之前的锁住所有的片段来统计数量,而是通过在put时每次对baseCount变量进行维护,但也只能拿到一个大概的值。

BlockingQueue(阻塞队列)

  1. 非高性能的队列并发容器,适用于生产者消费者模式中去使用
  2. 其内部通过ReentrantLock+Condition加以实现,当线程从队列中获取数据时,如果队列中没有数据,则会进行阻塞直到队列中有数据或是等待超时才返回线程,而当队列已经满了时则阻塞当前添加数据的线程,直到队列中出现未满或等待超时的情况。

ConcurentLinkedQueue

  1. 高性能的队列并发容器,采用大量非阻塞加cas操作实现高性能的并发容器
  2. 入队:队列中通过head和tail节点进行标记管理,head标记的是队列中的第一个节点,而tail节点可能是最后一个节点也可能是通过next指向尾节点,所以尾节点不一定就是tail节点。入队时如果tail的next指向为空证明tail节点为当前队列的尾节点,就将tail节点的next指向新加的节点,如果当前tail标记的节点的next指向不为空则说明当前tail标记的节点为队列的倒数第二个节点,需要将新加入的节点标记为tail节点。
  3. 出队:根据以上可知被head标记的即是队列的第一个节点,但是需要注意的是在出队过程中不是每次出队都会重新标记head节点,主要是减少资源开销。首先如果被head标记的节点不为空则直接返回该节点,就通过cas把head标记节点更新为null,当其他线程获取到head标记的节点为null或者cas操作失败时证明已经有其他线程进行了出队操作,则重新标记head节点重新获取对应的头节点。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值