ConCurrentHashMap源码学习

ConCurrentHashMap在JDK7之前是ReentrantLock+Segment+HashEntry,在JDK8及之后是synchronized+CAS+Node+红黑树。

JDK7之前

对于JDK7的版本实现,ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个Segment结构,一个Segment其实就是一个类Hash Table ,Segment内部维护一个链表数组。

        我们用下面一幅图看ConcurrentHashMap的内部结构从下面就可以看到ConcurrentHashMap定位一个元素需要进行两次Hash操作,第一次Hash定位到Segment,第二次定位到元素所在的链表的头部,因此这一种结构带来的副作用就是Hash的过程要比HashMap长。好处是操作的时候只需要对Segment进行操作即可,不会影响其他的Segment,这样在最理想的情况下ConcurrentHashMap可以最高同时支持Segment数量大小的写操作。所以,通过这一种结构,ConcurrentHashMap 的并发能力可以大大的提高。我们用下面这一幅图来看下ConcurrentHashMap的内部结构详情图,

        为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,所以,如果不是并发情形,不要使concurrentHashmap。

        JAVA7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在JAVA8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化.

JDK8之后

结构:数组+链表+红黑树。

重要属性和内部类

// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
 
// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
 
// hash 表
transient volatile Node<K,V>[] table;
 
// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;
 
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
 
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
 
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
 
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}

重要方法

// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
 
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
 
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)

构造器分析

可以看出来实现类懒惰性加载,在构造方法中仅仅计算了table的大小,以后在第一次使用时才会真正的创建

/**
    *loadFactor 负载因子0.75
    *concurrencyLevel 并发线程数
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
     if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
         throw new IllegalArgumentException();

     if (initialCapacity < concurrencyLevel) // Use at least as many bins
         initialCapacity = concurrencyLevel; // as estimated threads
     long size = (long)(1.0 + (long)initialCapacity / loadFactor);
      // tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ... 
     int cap = (size >= (long)MAXIMUM_CAPACITY) ?
             MAXIMUM_CAPACITY : tableSizeFor((int)size);
      this.sizeCtl = cap;
}

Put操作

  1. 如果没有初始化就先调用initTable()方法来进行初始化过程
  2. 如果没有hash冲突就直接CAS插入
  3. 如果还在进行扩容操作就先进行扩容
  4. 如果存在hash冲突,就加synchronized锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
  5. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
  6. 如果添加成功就调用addCount方法统计size,并且检查是否需要扩容
 final V putVal(K key, V value, boolean onlyIfAbsent) {
        //ConcurrentHashMap不支持键或值null
        if (key == null || value == null) throw new NullPointerException();

        //将key的hash值设置为正数,负数有别的作用

        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {

            Node<K,V> f; int n, i, fh;
            //如果tab数组不存在,或长度为0,就初始化table数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果数组的某个槽位上的节点为null,就new一个节点然后cas替换
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果槽位的头节点的hash值是Move是,就说明这个table在扩容,就帮助它扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //锁住头节点,对该节点下进行添加
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }

                if (binCount != 0) {
                    //如果binCount>= 一定的值就开始转换为tree
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        
        addCount(1L, binCount);
        return null;
    }

Get操作

注意:链表长度大于8&&table的容量大于64就转换为红黑树

  1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
  2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
  public V get(Object key) {

        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        保证key的hash码是一个正整数,负数有特殊用途
        int h = spread(key.hashCode());
        //tab不为null,长度不等于0,那个节点槽位不为null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果头节点==h,说明就是头节点符合
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //如果头节点hash是负数表示该bin在扩容forwordNode,或是treebin,这时候调用find方法查找
        //如果是forNode就去新的table中去寻找,treebin就进行红黑树的遍历
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //继续在链表的情况下查找
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

思考

想一想我们可以使用CocurrentHashMap来代替Hashtable吗?

因为HashTable是Synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁,ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性,他们都可以用于多线程的环境,但是HashTable增加到一定的时候,性能就会急剧下降因为迭代时需要被锁定很长的时间

         ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。

ConcurrentHashMap有什么缺陷吗?

        ConcurrentHashMap是非阻塞的,更新的时候会锁住局部部分数据。但不会把整个表都锁住。同步操作则是完全非阻塞的,好处是在保证合理的同步前提下,效率很高。

        坏处是严格来书读取操作不能反映最近的更新。弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历,

但ConcurrentHashMap不会失败,所以读取数据不能反映最新的更新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/636075.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

路由器不能端口映射什么原因?如何设置内网映射?

近期有小伙伴发来求助信息&#xff0c;他以前开游戏服务器和别人一起玩&#xff0c;那个时候端口映射还好&#xff0c;不知道哪一天开始突然不行了&#xff0c;已经是公网了&#xff0c;光猫是桥接的状态&#xff0c;连路由器都换了&#xff0c;就是不能端口映射开服务器&#…

如何使用Suno:免费的AI歌曲生成器

文章目录 Suno AI 是什么&#xff1f;Suno AI 如何工作&#xff1f;选择Suno AI的理由&#xff1a;核心优势易于操作多样化创作灵活的定价策略版权保障技术突破 如何使用Suno AI创作歌曲&#xff1f;第1步&#xff1a;注册Suno AI账户第2步&#xff1a;输入提示词创建第 3 步&a…

酷开系统 | 酷开科技把握智慧先机 AI赋能家庭场景

智慧化是当今世界科技发展的前沿领域之一。现在的智慧化&#xff0c;也正在逐步成为我们日常生活的一部分。电视系统也进入了数字化时代&#xff0c;AI的应用正在不断扩展&#xff0c;其潜力似乎无穷无尽。 酷开科技深耕人工智能技术&#xff0c;在提升语音体验、强化智能家居…

leetcode230 二叉搜索树中第K小的元素

题目 给定一个二叉搜索树的根节点 root &#xff0c;和一个整数 k &#xff0c;请你设计一个算法查找其中第 k 个最小元素&#xff08;从 1 开始计数&#xff09;。 示例 输入&#xff1a;root [5,3,6,2,4,null,null,1], k 3 输出&#xff1a;3 解析 这道题应该是能做出…

小程序丨数据功能如何使用

查询发布完成后&#xff0c;如发现信息有误或想要修改信息&#xff0c;老师可以使用数据功能在线修改已发布的查询内容。 数据功能包含导出、添加、编辑、更多操作&#xff0c;下面来教大家如何使用吧。 &#x1f4cc;使用教程 数据功能主要用于在线修改已发布的查询内容&#…

什么是流量削峰?如何解决秒杀等业务的削峰场景

文章推荐 1 作为程序员&#xff0c;开发用过最好用的AI工具有哪些&#xff1f; 2 Github Copilot正版的激活成功&#xff0c;终于可以chat了 3 idea,pycharm等的ai assistant已成功激活 4 新手如何拿捏 Github Copilot AI助手&#xff0c;帮助你提高写代码效率 5 Jetbrains的a…

软件设计:基于 python 代码快速生成 UML 图

1. 官方文档 PlantUML Language Reference Guide Comate | 百度研发编码助手 百度 Comate (Coding Mate Powered by AI) 是基于文心大模型的智能代码助手&#xff0c;结合百度积累多年的编程现场大数据和外部优秀开源数据&#xff0c;可以生成更符合实际研发场景的优质代码。…

【easyx】快速入门——弹球小游戏(第一代)

目录 1.需求 2.运动的小球 3.碰到边缘反弹 4.圆周撞击或越过边界反弹 5.绘制和移动挡板 6.小球碰到挡板反弹 7.游戏失败时该如何处理 8.随机初始条件 9.完整代码 我们这一节将结合动画和键盘交互的知识来做一个小游戏 1.需求 我们先看需求:小球在窗体内运动,撞到除…

OTP8脚-全自动擦鞋机WTN6020-低成本语音方案

一&#xff0c;产品开发背景 首先&#xff0c;随着人们生活质量的提升&#xff0c;对鞋子的保养需求也日益增加。鞋子作为人们日常穿着的重要组成部分&#xff0c;其清洁度和外观状态直接影响到个人形象和舒适度。因此&#xff0c;一种能够自动清洁和擦亮鞋子的设备应运而生&am…

docker 安装 SonarQube

文章目录 docker 安装 SonarQube一、修改句柄二、创建挂载文件夹三、拉取镜像四、修改 PG 库4.1、创建用户4.2、创建库 五、启动和挂载六、访问七、安装插件 docker 安装 SonarQube 版本&#xff1a;8.9 对 JDK 8 最大支持为 8.9 版本 一、修改句柄 #修改文件句柄数量&#…

Android Studio 版本升级后 Gradle project sync failed(Android V 应用升级)

问题及解决方案 更新到蜥蜴 Android Studio Iguana 后&#xff0c;出现Gradle project sync failed的问题&#xff08;IDE更新版本的常态了&#xff09;。 背景&#xff1a;对应用进行Android V版本升级&#xff08;SDK35&#xff0c;gradle插件版本要 8.4.0&#xff09; 1、…

安卓实现5个底部导航栏切换fragment

步骤&#xff0c;写 5 个 fragment 自定义的类5个布局文件&#xff1a; package com.xmkjsoft.xhgh.fragment;import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup;import androidx.annotation.NonNul…

每日一题——C++、Python实现牛客网CM11 链表分割(举一反三+思想解读+逐步优化)

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 题目链接 目录 我的写法 C嘎嘎 ​编辑 Python 代码点评 代码点评 时间复杂度分析 空…

飞凌嵌入式亮相上海CPSE,展现智能充储技术新力量

5月22日~24日&#xff0c;第三届上海国际充电桩及换电站展览会(CPSE)在上海汽车会展中心举行&#xff0c;飞凌嵌入式以“聚焦充电桩主控智造赋能车桩智联”为主题参展&#xff0c;与来自全国的客户朋友及行业伙伴一同交流分享&#xff0c;展位号Z15。 作为国内较早从事嵌入式技…

PDF24 Creator v11.12.1软件安装教程(附软件下载地址)

软件简介&#xff1a; 软件【下载地址】获取方式见文末。注&#xff1a;推荐使用&#xff0c;更贴合此安装方法&#xff01; PDF24 Creator v11.12.1是一款免费、简便实用的多功能 PDF 工具。用户可通过直观拖放界面轻松组合、编辑和处理PDF文件。功能包括合并、分割、添加、…

JVM学习-动态链接和方法返回地址

动态链接–指向运行时常量池的方法引用 每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用&#xff0c;包含这个引用的目的为了支持当前方法的代码能够实现动态链接(Dynamic Linking)&#xff0c;如invokednamic指令。在Java源文件被编译到字节码文件中时&#x…

异步爬虫学习实战项目:水效标识网

大家好&#xff0c;我是南枫&#xff0c;今天一起来学习异步爬虫。 文章开始之前&#xff0c;我们先搞清楚为什么要学异步爬虫&#xff1f;我们之后在工作中会遇到爬大量数据&#xff0c;比如百万数据采集&#xff0c;用平常的方法爬取的效率会比较低&#xff0c;所以要学习异…

AI应用案例:电能量异常分析智能诊断系统

窃电和计量装置故障造成漏收、少收电费使电力系统利益受损。一般情况主要通过定期巡检、定期校验电表、用户举报窃电等手段来发现窃电或计量装置故障。对人的依赖性太强&#xff0c;抓窃查漏的目标不明确。利用电力系统中逐步积累下来的海量真实数据&#xff0c;采用数据挖掘技…

CSDN智能总结助手

github项目地址&#xff1a; https://github.com/anjude/little-demo/tree/master 获取CSDN的user name和user token 打开csdn&#xff0c;打开控制台 - Application - Cookies&#xff0c;找到domain为blog.csdn.net的cookie&#xff0c;复制user_name和user_token的值 把上…

业务逻辑漏洞安全指南

业务逻辑漏洞安全指南 在当今互联的数字环境中&#xff0c;保护业务逻辑流程的安全对于维护企业应用程序的完整性、机密性和可用性至关重要。业务逻辑漏洞可能被利用来执行未经授权的操作、破坏服务或窃取敏感数据。 1. 身份验证、会话管理和访问控制 所有高价值的业务逻辑流…