并发容器(二):Concurrent类下的ConcurrentHashMap源码级解析

并发容器-ConcurrentHashMap

    • 前言
    • 数据结构
      • JDK1.7版本
        • HashEntry
        • Segment
      • 初始化
    • 重要方法
      • Put方法
        • 扩容rehash方法

前言

之前我们在集合篇里聊完了HashMap和HashTable,我们又学习了并发编程的基本内容,现在我们来聊一聊Concurrent类下的重要容器,ConcurrentHashMap。
HashTable被逐渐废弃,离不开ConcurrentHashMap的出现,可以想象HashMap做为一个高频使用的集合框架,如果每次使用过程中都将整个方法synchronized,这样意味着全局加锁,肯定会导致并发的低效,所以ConcurrentHashMap的出现,改变了这种情况,接下来我们来看一看ConcurrentHashMap的神奇之处。

数据结构

JDK1.7版本

ConcurrentHashMap在JDK1.7中,提供了一种颗粒度更细的加锁机制.
这种机制叫分段锁(Lock Sriping).
整个哈希表被分为了多个段,每个段都能独立锁定.

机制的优点:读取操作不需要锁,写入操作仅锁定相关的段.这减小了锁冲突的概率,从而提高了并发性能.
在并发环境下实现更高的吞吐量,在单线程环境下只损失非常小的性能.
在这里插入图片描述
(图片转载自网络)

HashEntry
static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

这个老朋友我们在介绍HashMap的时候应该非常熟悉它了.不太明白的朋友可以翻看一下HashMap那一章.

Segment
static final class Segment<K,V> extends ReentrantLock implements Serializable {

        private static final long serialVersionUID = 2249069246763182397L;

        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
        transient volatile HashEntry<K,V>[] table;
        transient int count;
        transient int modCount;
        transient int threshold;
        final float loadFactor;
        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
}

从上面代码可以看到,有一个volatile修饰的HashEntry数组
所以可以看出,Segment的结构和HashEntry是非常类似的,都是一种数据和链表相结合的结构.
即每个Segment守护着n个HashEntry,而每一个HashEntry本身可以当作一条链表连接n个HashEntry.

那么可以用图来表示一下segment和hashEntry的关系
在这里插入图片描述

初始化

public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

无参构造里面调用了有参构造,传入了三个参数的默认值,它们的值是:

// 默认初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 16;

// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 默认并发级别
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
我们可以看到,这个也是指segment数,默认是16.
    @SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        //上来先做参数校验
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
            //校验并发级别的大小,如果大于1<<16,重置为65536
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        //2的多少次方
        int sshift = 0;
        int ssize = 1;
        //这个循环目的是可以找到concurrencyLevel之上最近的2的次方值
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        //记录偏移量
        this.segmentShift = 32 - sshift;
        //记录段掩码
        this.segmentMask = ssize - 1;
        //设置容量,如果大于最大值,则设置为最大值
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
            //平摊到每个segment中可以分到的大小,如果initialCapacity为64,那么每个segment可以分到4个
            //计算每个segment中类似HashMap的容量
        int c = initialCapacity / ssize;
        //如果c*ssize<initialCapacity,说明上面除的时候有余数,下面给c++就好啦
        if (c * ssize < initialCapacity)
            ++c;
            //设置segment中最小的容量值,这里默认为2,确保插入一个不会扩容
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        //segment中类似于HashMap,容量至少为2或者2的倍数
        while (cap < c)
            cap <<= 1;
        // 创建 segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        /往数组中写入segment[0]
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

重要方法

Put方法

 public V put(K key, V value) {
        Segment<K,V> s;
        //校验传入的value是否为空
        if (value == null)
            throw new NullPointerException();
            //根据传入的key计算出hash值
        int hash = hash(key);
        //根据hash值找到segment数组中位置j
        //hash值是32位,无符号右移segmentShift位,剩下高四位.
        //然后和segmentMask(15)做一次与操作,也就说说j是hash值的高四位,也就说槽点数组下标
        int j = (hash >>> segmentShift) & segmentMask;
        //这里使用unsave类的getObject方法从segment数组中检索段.
        //(j是要检索的段段索引
        //SSHIFT用于计算偏移量的位移量
        //SBASE用于计算基本偏移的基本偏移量
        //这里判断是否位null.如果是null则调用ensureSegment对segment[j]进行初始化
        if (
        (s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null
        ) 
            s = ensureSegment(j);
            //插入新值到槽s中
        return s.put(key, hash, value, false);
    }

然后继续看下一层的put代码

 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
 //在往segment写入前,需要先获取该segment的独占锁
            HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
            //segment内部的数组
                HashEntry<K,V>[] tab = table;
                //利用hash值算出防止数组下标
                int index = (tab.length - 1) & hash;
                //first是数组该位置处链表的表头
                HashEntry<K,V> first = entryAt(tab, index);
                //这个for循环主要是考虑一些情况: 已经存在链表和没有任何元素这两种情况.
                for (HashEntry<K,V> e = first;;) {
                //此时e不为null
                    if (e != null) {
                        K k;
                        //复合赋值比较,用于检查当前节点的键是否与待插入键相同,并在比较的同时完成了一次赋值操作。
                        //前面成立(即引用相等)时,确实后面不需要再判断,逻辑上已经确定了键相等。但如果设计中考虑到可能有内容相等但引用不同的情况,后面的判断仍然是必要的逻辑完整性的一部分,确保所有相等的情况都被正确处理。
                        //扩展
/**引用相等:(k = e.key) == key 判断的是传入的键 key 和链表中节点的键 e.key 是否是同一个对象实例。如果它们是同一个对象(引用相同),那么直接就可以确认键相等,无需进一步比较。

内容相等:但是,如果键是根据内容定义相等而不是引用(比如自定义的键类重写了 .equals() 方法),那么即使 == 返回 false,键的内容仍然可能相等(通过 .equals() 判断)。因此,(e.hash == hash && key.equals(k)) 部分是用来处理这种情况的,它确保即使两个键不是同一个实例,如果它们的内容相等(通过 .equals() 方法判断),也被视为相同的键。**/
                        if ((k = e.key) == key || (e.hash == hash && key.equals(k))) 
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                            //覆盖
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                    //如果不为null,那么就把插入的值设置为链表表头(头插法)
                        if (node != null)
                        //这里设置一下引用节点
                            node.setNext(first);
                            //如果为null,初始化并设置为链表表头
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //如果超过segment的阈值,这个segment需要扩容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                        //如果没有达到阈值,将node放到数组tab的index位置
                        //换句话说将新的节点设置为原链表的表头
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

扩容rehash方法
  private void rehash(HashEntry<K,V> node) {
            HashEntry<K,V>[] oldTable = table;
             //旧容量
            int oldCapacity = oldTable.length;
            //新容量,扩大2倍
            int newCapacity = oldCapacity << 1;
            //计算新的扩容阈值
            threshold = (int)(newCapacity * loadFactor);
            //创建新的数组
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
                //计算新掩码 容量-1
            int sizeMask = newCapacity - 1;
           //遍历老数据
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                //如何能拿到值
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    //计算新位置,新位置只有两种情况:不变或者老位置+老容量
                    int idx = e.hash & sizeMask;
                    if (next == null)   //  Single node on list
                    //当前位置不是链表只是元素,直接赋值
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                    //如果是链表
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        //这里的for循环目的是:找到一个特殊节点,这个节点后所有next节点的新位置都是相同的
                        /**
                        这块我再解释一下,容器扩容前后是只会有两个位置,一个新位置,一个旧位置,这里的计算是把放在新位置的索引的链表找出来啦,然后新位置的链表都是要放一起的.
                        **/
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        //将lastRun及其之后的所有节点组成的链表放在lastIdx这个位置
                        newTable[lastIdx] = lastRun;
                        // 下面操作处理lastRun之前的节点
                        //这些节点可能分配在另外一个链表上,也有可能分配到上面那个链表中
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            //新来的node放在新数组中刚刚两根链表之一的头部
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

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

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

相关文章

tsp可视化python

随机生成点的坐标并依据点集生成距离矩阵&#xff0c;通过点的坐标实现可视化 c代码看我的这篇文章tsp动态规划递归解法c from typing import List, Tuple import matplotlib.pyplot as plt from random import randintN: int 4 MAX: int 0x7f7f7f7fdistances: List[List[in…

最长不下降子序列LIS详解

最长不下降子序列指的是在一个数字序列中&#xff0c;找到一个最长的子序列&#xff08;可以不连续&#xff09;&#xff0c;使得这个子序列是不下降&#xff08;非递减&#xff09;的。 假如&#xff0c;现有序列A[1&#xff0c;2&#xff0c;3&#xff0c;-1&#xff0c;-2&…

60.WEB渗透测试-信息收集- 端口、目录扫描、源码泄露(8)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;59.WEB渗透测试-信息收集- 端口、目录扫描、源码泄露&#xff08;7&#xff09; 御剑是用…

植物大战僵尸杂交版全新版v2.1解决全屏问题

文章目录 &#x1f68b;一、植物大战僵尸杂交版❤️1. 游戏介绍&#x1f4a5;2. 如何下载《植物大战僵尸杂交版》 &#x1f680;二、解决最新2.1版的全屏问题&#x1f308;三、画质增强以及减少闪退 &#x1f68b;一、植物大战僵尸杂交版 《植物大战僵尸杂交版》是一款在原版《…

【Android】三种常见的布局LinearLayout、GridLayout、RelativeLayout

【Android】三种常见的布局LinearLayout、GridLayout、RelativeLayout 在 Android 开发中&#xff0c;布局&#xff08;Layout&#xff09;是构建用户界面的基础。通过合理的布局管理&#xff0c;可以确保应用在不同设备和屏幕尺寸上都能有良好的用户体验。本文将简单介绍 And…

困惑度作为nlp指标的理解示例

为了更清晰地说明困惑度的计算过程以及如何通过困惑度判断模型的优劣&#xff0c;我们可以通过一个简单的例子来演示。假设我们有一个非常简单的文本语料库和两个基础的语言模型进行比较。 示例文本 假设我们的文本数据包括以下两个句子&#xff1a; “cat sits on the mat”…

蔡崇信“预言”:微软与OpenAI未来极有可能会分道扬镳

近日&#xff0c;在美国投行摩根大通于上海举行的第二十届全球中国峰会上&#xff0c;阿里巴巴集团联合创始人、董事局主席蔡崇信与摩根大通北亚区董事长兼大中华区投资银行业务副主席关金星&#xff08;Kam Shing Kwang&#xff09;进行了一场精彩对话。蔡崇信深入分享了他对公…

线上教育培训办公系统系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;教师管理&#xff0c;学生管理&#xff0c;运营事件管理 教师账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;作业管理&#xff0c;电…

OpenCore 引导完美升级

备份原有 OC (做好回滚的准备下载新版 OpenCore https://github.com/acidanthera/OpenCorePkg/releases将 1, 3, 4 里面的文件使用新版进行替换 4 里面的文件严格来说并不需要, 只是留着方便使用不追求完美到这就可以收工了将 OC 复制到 U 盘 EFI U 盘格式化可以使用: diskutil…

js 用正则表达式 匹配自定义字符之间的字符串数据,如:( )、[ ]、{ }、< >、【】等括号之间的字符串数据

要使用正则表达式匹配尖括号()之间的数据&#xff0c;可以使用以下代码示例&#xff1a; 在JavaScript中&#xff0c;你可以使用正则表达式来匹配括号()之间的数据。以下是一个简单的例子&#xff0c;它展示了如何使用正则表达式来获取两对括号之间的文本。 // 示例字符串 con…

Spring-kafka消费者消费的一些问题

前言 Spring Kafka 无缝集成了 Spring Boot、Spring Framework 及其生态系统中的其他项目&#xff0c;如 Spring Cloud。通过与 Spring Boot 的自动配置结合&#xff0c;开发者可以快速启动和配置 Kafka 相关的功能。无需编写大量样板代码即可实现 Kafka 的生产和消费功能&…

现在用U盘的人还多吗?多用于哪些场景?

在公司中使用U盘的人仍然相当多&#xff0c;主要在以下场景下使用&#xff1a; 数据存储与备份&#xff1a;U盘作为一种便携式存储设备&#xff0c;被广泛应用于数据的存储和备份。对于需要经常在不同设备或地点之间传输数据的员工来说&#xff0c;U盘提供了一个方便、快捷的解…

如何使用ios自带语音转文字工具?

ios自带语音转文字是iOS系统中自带的语音转文字功能主要应用于以下几个方面&#xff1a; 1. 语音输入&#xff1a;在iOS的任何文本输入框中&#xff0c;通常都有一个麦克风图标&#xff0c;点击后可以进行语音输入&#xff0c;系统会将你的语音实时转换成文字。 2. Siri&…

ESD与EOS区别

最近小白在做项目时&#xff0c;被一个实习生问道了&#xff0c;关于EOS与ESD区别。说实话&#xff0c;以前专注于测试debug的我&#xff0c;在回答对方时&#xff0c;并没法做到太全面的解答。于是乎&#xff0c;借助周内的空闲时间&#xff0c;小白还是简单学习总结了一番。 …

OceanBase 金融项目优化案例

领导让我帮忙支持下其他项目的SQL优化工作&#xff0c;呦西&#xff0c;是收集案例的好机会。&#x1f60d; 下面SQL都是在不能远程的情况下&#xff0c;按照原SQL的逻辑等价改写完成发给现场同学验证。 案例一 慢SQL&#xff0c;4.32秒&#xff1a; SELECT MY_.*, RM FROM (SE…

【MATLAB】(高数)

参考文章 函数极限 导数与偏导 极值和最值 局部范围的最值 局部范围内的最值&#xff0c;相当于函数的极值 离散数据的最值 多元函数的极值 fminunc [x, fval] fminunc(fun, x0)fun为代求极值的函数&#xff1b;x0为起始点&#xff0c;即从这个点开始寻找极值&#xff0c;…

Ui学习--UITableView

UI学习 UITableView基础UITableView协议UITableView高级协议与单元格总结 UITableView基础 UITableView作为iOS中的一个控件&#xff0c;用于以表格形式展示数据。例如通讯录好友&#xff0c;朋友圈信息等&#xff0c;都是UITableView的实际运用场景。 首先我们先要加入两个协…

苹果加大AI布局,上海新店开业昭示中国市场新动向

随着全球科技巨头纷纷进军人工智能领域&#xff0c;苹果公司亦不甘示弱&#xff0c;近期在上海静安新店的开业以及CEO蒂姆库克的一系列动作&#xff0c;都显示出苹果在AI方面的雄心壮志。这不仅是对未来技术趋势的积极回应&#xff0c;更是对市场竞争态势的精准把握。 库克的访…

CSS从入门到精通——动画:CSS3动画延迟和完成后状态的保持

目录 任务描述 相关知识 动画状态 动画完成时的状态 动画延迟 编程要求 任务描述 本关任务&#xff1a;用 CSS3 实现小车等待红绿灯的效果。效果图如下&#xff1a; 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;1.动画状态&#xff0c;2.动画完成时的状…

奥特曼谈AI的机遇、挑战与人类自我反思:中国将拥有独特的大语言模型

奥特曼在对话中特别提到&#xff0c;中国将在这个领域扮演重要角色&#xff0c;孕育出具有本土特色的大语言模型。这一预见不仅彰显了中国在全球人工智能领域中日益增长的影响力&#xff0c;也预示着未来技术发展的多元化趋势。 ①奥特曼认为AI在提升生产力方面已显现积极作用&…