专栏首页chenchenchenConcurrentHashMap(JDK8)

ConcurrentHashMap(JDK8)

对比

与HashMap的区别是什么?

ConcurrentHashMap是HashMap的升级版,HashMap是线程不安全的,而ConcurrentHashMap是线程安全。而其他功能和实现原理和HashMap类似。

与Hashtable的区别是什么?

Hashtable也是线程安全的,但每次要锁住整个结构,并发性低。相比之下,ConcurrentHashMap获取size时才锁整个对象。

Hashtable对get/put/remove都使用了同步操作。ConcurrentHashMap只对put/remove同步。

Hashtable是快速失败的,遍历时改变结构会报错ConcurrentModificationException。ConcurrentHashMap是安全失败,允许并发检索和更新。

JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?

  1. JDK8中新增了红黑树
  2. JDK7中使用的是头插法,JDK8中使用的是尾插法
  3. JDK7中使用了分段锁,而JDK8中没有使用分段锁了
  4. JDK7中使用了ReentrantLock,JDK8中没有使用ReentrantLock了,而使用了Synchronized
  5. JDK7中的扩容是每个Segment内部进行扩容,不会影响其他Segment,而JDK8中的扩容和HashMap的扩容类似,只不过支持了多线程扩容,并且保证了线程安全

特性

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

使用场景

多用户同时登入和登出

// 在线用户管理类
public class UserManager {
    private Map<String, User> userMap = new ConcurrentHashMap<>();
    
    // 当用户登入时调用
    public void onUserSignIn(String sessionId, User user) {
        this.userMap.put(sessionId, user);
    }
    
    // 当用户登出或超时时调用
    public void onUserSignOut(String sessionId) {
        this.userMap.remove(sessionId);
    }
    
    public getUser(String sessionId) {
        return this.userMap.get(sessionId);
    }
}

统计文本单词

多线程统计文本单词,下面代码会出现BUG

ConcurrentHashMap map  = new ConcurrentHashMap<String,Integer>();
​
//下面多线程运行,会出现BUG
Integer value= map.get(word);
if (value==null){
    map.put(word,1);
}else {
    map.put(word,value++);
}computeIfAbsent

ConcurrentHashMap可以保证单个get/put操作的原子性,但是不能保证两个一起就是原子性。

如何解决? ConcurrentHashMap提供了两个方法

  • computeIfAbsent:计算如果不存在。如果key不存在,存入计算结果并返回
  • computeIfPresent:计算如果存在。如果key存在,计算公式并返回
ConcurrentHashMap<String,Integer> map  = new ConcurrentHashMap<>();
Integer a1 = map.computeIfAbsent("a", (key) -> 1+1);
Integer a2 = map.computeIfPresent("a", (key,value) -> map.get(key)+value);
System.out.println(a1);// 2
System.out.println(a2);// 4

将Integer替换为原子类LongAdder,解决多线程a++问题即可。

存储线程资源池,为每种请求类型创建一个线程池

初始化一个成员变量map。当请求到达时,检查map中是否已经存在创建好的线程池即可,如果存在则返回,如果不存在就创建一个新的线程池放入map中,同时返回新创建的线程池。

public ThreadPool getThreadPool(String type) {
​
    RingBuffer<StringEvent> disruptor = threadPoolMap.get(type);
    if (disruptor == null) {
        synchronized (this) {
            disruptor = threadPoolMap.get(type);
            if (disruptor == null) {
                threadPoolMap.put(type, createThreadPool(type));
            }
        }
    }
    return threadPoolMap.get(type);
​
}

使用ConcurrentHashMap的性能会比单纯的使用synchronized+hashMap高很多。

   public ThreadPool getThreadPool(String type) {
​
    RingBuffer<StringEvent> disruptor = threadPoolMap.get(type);
    if (disruptor == null) {
        threadPoolMap.computeIfAbsent(type, (key) -> createThreadPool(type));
       }
    }
    return threadPoolMap.get(type);

读超过写,作为缓存

CHM适用于做cache,在程序启动时初始化,之后可以被多个请求线程访问。

  • 当写者数量大于等于读者时,CHM的性能是低于Hashtable和synchronized Map的。
  • 因为当锁住了整个Map时,读操作要等待对同一部分执行写操作的线程结束。
  • CHM是HashTable一个很好的替代,但CHM的比HashTable的同步性稍弱。

获取操作get与更新操作交迭(包括 put 和 remove)

遍历过程中,集合结构变化,不会抛出ConcurrentModificationException,能够正常遍历完成。

原因:

1、读写不互斥,其他线程修改容器中部分副本时,读操作不受影响。

2、hapend-before机制,避免读取到更新前的数据。

3、读写机制通过violatile实现,迭代时、数组扩容时保证数据的可见性,不会出现数组越界等异常。

源码解析:

参考:

关于jdk1.8中ConcurrentHashMap的方方面面:https://blog.csdn.net/tp7309/article/details/76532366

ConcurrentHashMap源码分析(JDK1.8):https://blog.csdn.net/ji1162765575/article/details/111309612

ConcurrentHashMap为何不会出现ConcurrentModificationException异常:https://www.bbsmax.com/A/6pdDgqbq5w/

concurrentHashMap对concurrentModificationException的处理:https://www.jianshu.com/p/0b769a8779f6

ConcurrentHashMap源码分析(JDK8) get/put/remove方法分析:https://www.jianshu.com/p/5bc70d9e5410

ConcurrentHashMap的错误使用:https://zhuanlan.zhihu.com/p/113379816

什么时候使用ConcurrentHashMap:https://my.oschina.net/u/3847203/blog/3084619

ConcurrentHashMap的使用场景:https://blog.csdn.net/a_fengzi_code_110/article/details/61191591

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 详解ConcurrentHashMap及JDK8的优化

    由于HashMap在并发中会出现一些问题,所以JDK中提供了并发容器ConcurrentHashMap。有关HashMap并发中的问题和原理,强烈建议查看这篇文...

    全菜工程师小辉
  • 深入理解并发容器-ConcurrentHashMap(JDK8版本)1 概述3应用场景4 源码解析

    JavaEdge
  • JDK8的ConcurrentHashMap源码学习笔记

    itliusir
  • 不止JDK7的HashMap,JDK8的ConcurrentHashMap也会造成CPU 100%

    大家可能都听过JDK7中的HashMap在多线程环境下可能造成CPU 100%的现象,这个由于在扩容的时候put时产生了死链,由此会在get时造成了CPU 10...

    java架构师
  • 不止 JDK7 的 HashMap ,JDK8 的 ConcurrentHashMap 也会造成 CPU 100%?原因与解决~

    大家可能都听过JDK7中的HashMap在多线程环境下可能造成CPU 100%的现象,这个由于在扩容的时候put时产生了死链,由此会在get时造成了CPU 10...

    芋道源码
  • BTA 常问的 Java基础40道常见面试题及详细答案

    最近看到网上流传着,各种面试经验及面试题,往往都是一大堆技术题目贴上去,而没有答案。

    搜云库
  • 深入浅出ConcurrentHashMap内部实现

    ConcurrentHashMap可以说是目前使用最多的并发数据结构之一,作为如此核心的基本组件,不仅仅要满足我们功能的需求,更要满足性能的需求。而实现一个高性...

    敖丙
  • 高并发编程系列:ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)

    HashMap、CurrentHashMap 的实现原理基本都是BAT面试必考内容,阿里P8架构师谈:深入探讨HashMap的底层结构、原理、扩容机制深入谈过h...

    麦克劳林
  • 【JDK】:ConcurrentHashMap高并发机制——【转载】

    在学习ConcurrentHashMap的高并发时,找到了一些高质量的博客,就没有重复转载了。分别列出了JDK6中的Segment分段加锁机制和JDK8中的CA...

    GavinZhou
  • 探究Java的ConcurrentHashMap实现机制

    原文地址: http://blog.csdn.net/u011080472/article/details/51392712

    GavinZhou
  • 理解Java7和8里面HashMap+ConcurrentHashMap的扩容策略

    (2)在(1)的基础上,理解ConcurrentHashMap的并发安全的设计和实现思路

    我是攻城师
  • JDK之ConcurrentHashMap 原

        注:分析JDK8的ConcurrentHashMap,JDK6/7上的实现和JDK8上的不一样。

    克虏伯
  • 被问到的JDK8新特性

    Java从已经从JDK1.0版本发展到了最新的JDK13, 为什么目前Jdk8经常被问到呢?

    用户8639654
  • 如何阅读jdk源码?

    这篇文章主要讲述jdk本身的源码该如何阅读,关于各种框架的源码阅读我们后面再一起探讨。

    彤哥
  • 还在为怎么阅读 JDK 源码犯愁吗?

    这篇文章主要讲述jdk本身的源码该如何阅读,关于各种框架的源码阅读我们后面再一起探讨。

    zhisheng
  • Java的ConcurrentHashMap

    ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。

    用户3467126
  • 深入理解ConcurrentHashMap

    前言 本文的分析的源码是JDK8的版本,上一节我们介绍了HashMap,提到了多线程避免扩容时出现死循环,时要使用ConcurrentHashMap,下面我来讲...

    胖虎
  • 并发容器和队列

    在我们开发中,经常会使用到容器来存储对象或数据,容器的作用非常大,合理使用各个容器的特性和方法可以简化开发,提高系统性能和安全性。

    胖虎
  • 理解Java8并发工具类ConcurrentHashMap的实现

    前面的文章已经分析过List和Queue相关的接口与并发实现类,本篇我们来分析一下非常Java里面非常重要的一个数据结构HashMap。(注意Set类型在这里我...

    我是攻城师

扫码关注云+社区

领取腾讯云代金券