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

news2024/7/4 4:25:05

并发容器-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;
        }

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

程序设计实践--字符串

字符串 回文串 “回文”是指正读反读都能一样的句子,它是古今中外都有的一种修辞方式和文字游戏。你的任务是从键盘输入一个字符串(最大长度<100),判断这个字符串是不是回文,如果是回文,则输出“Yes”, 如果不是, 则输出“No”。 输入描述 输入若干个字符串(最大长…

[C++]使用yolov10的onnx模型结合onnxruntime和bytetrack实现目标追踪

【官方框架地址】 yolov10yolov10框架&#xff1a;https://github.com/THU-MIG/yolov10 bytetrack框架&#xff1a;https://github.com/ifzhang/ByteTrack 【算法介绍】 Yolov10与ByTetrack&#xff1a;目标追踪的强大组合 Yolov10和ByTetrack是两种在目标追踪领域具有显…

【计算机组成原理】指令系统考研真题详解之拓展操作码!

计算机组成原理&#xff1a;指令系统概述与深入解析 1. 指令系统概述 计算机软硬件界面的概念 在计算机组成原理中&#xff0c;指令系统扮演着至关重要的角色&#xff0c;它是计算机软硬件界面的核心。软件通过指令与硬件进行通信&#xff0c;硬件根据指令执行相应的操作。指…

韩顺平0基础学java——第24天

p484-508 System类 常见方法 System.arrycopy&#xff08;src&#xff0c;0&#xff0c;dest&#xff0c;1,2&#xff09;&#xff1b; 表示从scr的第0个位置拷贝2个&#xff0c;放到目标数组索引为1的地方。 BigInteger和BigDecimal类 保存大整数和高精度浮点数 BigInte…

Unity的三种Update方法

1、FixedUpdate 物理作用——处理物理引擎相关的计算和刚体的移动 (1) 调用时机&#xff1a;在固定的时间间隔内&#xff0c;而不是每一帧被调用 (2) 作用&#xff1a;用于处理物理引擎的计算&#xff0c;例如刚体的移动和碰撞检测 (3) 特点&#xff1a;能更准确地处理物理…

RIP路由附加度量值(华为)

#交换设备 RIP路由附加度量值 RIP&#xff08;Routing Information Protocol&#xff09;路由协议中的附加度量值是指在RIP路由原来度量值的基础上所增加的额外度量值&#xff0c;通常以跳数来表示。这个附加度量值可以是正值&#xff0c;也可以是负值&#xff0c;用于影响路…

嵌入式学习记录6.14(练习)

#include "mainwindow.h" #include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this);this->resize(1028,783); //设置左侧背景QLabel *lab1new QLabel(this);lab1->…

慈善组织管理系统设计

一、用户角色与权限 慈善组织管理系统设计首先需要考虑的是用户角色与权限的划分。系统应明确区分不同的用户角色&#xff0c;如管理员、项目负责人、财务人员、捐赠者等&#xff0c;并为每个角色分配相应的权限。管理员应拥有最高的权限&#xff0c;能够管理系统全局&#xf…

集成学习方法:Bagging与Boosting的应用与优势

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

嵌入式数据库的一般架构

嵌入式数据库的架构与应用对象紧密相关&#xff0c;其架构是以内存、文件和网络等三种方式为主。 1.基于内存的数据库系统 基于内存的数据库系统中比较典型的产品是每个McObject公司的eXtremeDB嵌入式数据库&#xff0c;2013年3月推出5.0版&#xff0c;它采用内存数据结构&…

redis 缓存jwt令牌设置更新时间 BUG修复

大家好&#xff0c;今天我又又又来了&#xff0c;hhhhh。 上文中 我们永redis缓存了token 但是我们发现了 一个bug &#xff0c;redis中缓存的token 是单用户才能实现的。 就是 我 redis中存储的键 名 为token 值 是jwt令牌 &#xff0c;但是如果 用户a 登录 之后 创建一个…

动态规划日常刷题

力扣70.爬楼梯 class Solution {public int climbStairs(int n) {return dfs(n);}//递归 //注意每次你可以爬 1 或 2 个台阶//如果最后一步是1 就先爬到n-1 把它缩小成0-n-1的范围//如果最后一步是2 就先爬到n-2 把它缩小成0-n-2的范围 private int dfs(int i){if(i < 1){r…

EarMaster 7.2. 54中文永久下载和安装激活使用指南

视唱练耳是每一个音乐艺考生都需要掌握的基础技能&#xff0c;要求学生通过学习掌握对音程、和弦以及单音的听辨能力。不过学习的过程不是一蹴而就的&#xff0c;除了需要大量的时间以外&#xff0c;还需要一些视唱练耳软件辅助学习。今天给大家介绍&#xff0c;能听辨音程的软…

EasyRecovery2024手机数据恢复的神器,你值得拥有!

EasyRecovery是一款超级实用的数据恢复软件&#xff0c;它能够帮助你找回不小心删除、格式化或丢失的数据。无论是照片、音乐、视频&#xff0c;还是重要的工作文件&#xff0c;EasyRecovery都能帮助你轻松找回。 而且&#xff0c;EasyRecovery的操作非常简单&#xff0c;即使…

OpenCV中 haarcascades 级联分类器各种模型.xml文件介绍

haarcascades Haar Cascades 是一种用于对象检测的机器学习模型&#xff0c;特别是在OpenCV库中广泛使用。这些模型通过训练大量的正样本&#xff08;包含目标对象的图像&#xff09;和负样本&#xff08;不包含目标对象的图像&#xff09;来识别图像中的对象。Haar Cascades …

索引失效有效的11种情况

1全职匹配我最爱 是指 where 条件里 都是 &#xff0c;不是范围&#xff08;比如&#xff1e;,&#xff1c;&#xff09;&#xff0c;不是 不等于&#xff0c;不是 is not null&#xff0c;然后 这几个字段 建立了联合索引 &#xff0c;而且符合最左原则。 那么就要比 只建…

如何让视频有高级感 高级感视频制作方法 高级感视频怎么剪 会声会影视频剪辑制作教程 会声会影中文免费下载

高质量视频通常具有清晰的画面、优质的音频和令人印象深刻的视觉效果。这篇文章来了解如何让视频有高级感&#xff0c;高级感视频制作方法。 一、如何让视频有高级感 要让视频有高级感&#xff0c;要注意以下几个要点&#xff1a; 1、剧本和故事性&#xff1a;一个好的剧本和…

ATMEGA16读写24C256

代码&#xff1a; #include <mega16.h> #include <stdio.h> #include <i2c.h> #include <delay.h> // Declare your global variables here #define EEPROM_BUS_ADDRESS 0xa0 #asm.equ __i2c_port0x15.equ __sda_bit1 .equ __scl_bit0 #endasm uns…

C语言程序设计-6 循环控制

C语言程序设计-6 循环控制 循环结构是程序中一种很重要的结构。其特点是&#xff0c;在给定条件成立时&#xff0c;反复执行某程序 段&#xff0c;直到条件不成立为止。给定的条件称为循环条件&#xff0c;反复执行的程序段称为循环体。&#xff23;语 言提供了多种循环语句&a…

【Python/Pytorch - 网络模型】-- TV Loss损失函数

文章目录 文章目录 00 写在前面01 基于Pytorch版本的TV Loss代码02 论文下载 00 写在前面 在医学图像重建过程中&#xff0c;经常在代价方程中加入TV 正则项&#xff0c;该正则项作为去噪项&#xff0c;对于重建可以起到很大帮助作用。但是对于一些纹理细节要求较高的任务&am…