注:本博客资源来自于享学课堂,自己消化之后有修改

目录

使用

在1.7下ConcurrentHashMap实现分析

在1.7下的实现原理图

构造方法和初始化

get操作

put操作

扩容rehash操作:对table扩容

remove操作

ConcurrentHashMap的弱一致性

在多线程下避免使用size、containsValue方法

在1.8下ConcurrentHashMap实现分析

图解

核心数据结构和属性

node

TreeNode

TreeBin

特殊的ForWardingNode

sizeCtl:用来控制table的初始化和扩容操作

核心方法

构造方法

get操作

put操作

initTable初始化table

多线程扩容transfer方法

remove

treeifyBin

size


使用

除了map有的线程安全的put和get方法外,ConcurrentHashMap还有在并发下的public V putIfAbsent(K key, V value),如果key已经存在,直接返回value的值,不会进行替换。如果key不存在,就添加key和value,返回null,并且它是线程安全的。

在1.7下ConcurrentHashMap实现分析

在1.7下的实现原理图

备注:建议有想画这种图的同学,可以使用processon,非常好用

ConcurrentHashMap是有Segment数组结构和HashEntry数组组成,hashEntry每个元素是链表;Segment继承ReentrantLock,是一种可重入锁,ConcurrentHashMap上锁就是在Segment上。

HashEntry是一个数组,每个数组元素存放的是一个链表,每次对链表元素进行修改的时候,都必须获得数组对应的Segment锁

构造方法和初始化

public ConcurrentHashMap17(int initialCapacity,float loadFactor, int concurrencyLevel)

参数说明

参数 描述 默认值
initialCapacity ConcurrentHashMap初始容量,默认是 DEFAULT_INITIAL_CAPACITY=16
loadFactor 负载因子阈值,用于控制大小调整。initailCapacity*loadFactor=HashMap的容量,负载越大,链表的数据越多,查找难度就会变大,负载越小,链表数据越少。 DEFAULT_LOAD_FACTOR = 0.75f
concurrencyLevel

估计并发级别,即可能会有多少个线程共同修噶爱这个map,以此来确定Segment数组大小,默认是16,必须是2的备注,如果设置17,正是就是32

DEFAULT_CONCURRENCY_LEVEL = 16

get操作

get操作先经过一次再散列的到A,然后通过|A高位获取到Senment[]的位置,然后通过A全部散列定位到在table[]中的位置。整个过程没有加锁,而是通过volatile保证get可以拿到最新值。

transient volatile HashEntry<K,V>[] table;
    public V get(Object key) {Segment<K,V> s; HashEntry<K,V>[] tab;//准备定位hash的值int h = hash(key);long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&//拿到segment下的table数组(tab = s.table) != null) {for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile//遍历table下的HashEntry链表(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);e != null; e = e.next) {K k;if ((k = e.key) == key || (e.hash == h && key.equals(k)))return e.value;}}return null;}

put操作

ConcurrentHashMap初始化的时候,会初始化Segment[0],其他的Segment在插入第一个值的时候才会初始化

    public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();//定位所需要的hash值int hash = hash(key);//定位到元素在Segment[]数组中的位置int j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K,V>)UNSAFE.getObject         (segments, (j << SSHIFT) + SBASE)) == null)//初始化Segment[j],因为整个map初始化的时候,只初始化了segment[0]s = ensureSegment(j);//把元素放到对应的Segment元素中return s.put(key, hash, value, false);}

s = ensureSegment(j);

多个线程进入同一个Segment[k],只要有一个成功就行了,使用CAS保证并发

s.put(key, hash, value, false);

Segment.put方法会舱室获得锁,如果没有获得所,调用scanAndLockForPut方法自旋等待获得锁

        final V put(K key, int hash, V value, boolean onlyIfAbsent) {HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);V oldValue;

scanAndLockForPut通过while自旋获取锁

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {。。。while (!tryLock()) {。。。}return node;
}

获取锁之后,如果摸个HashEntry节点有相同的key,就更新HashEntry的value值;否则新建已给HashEntry节点,采用头插法把他设置为链表的新head节点,并将原节点设置为head的下一个节点

新建过程中如果节点数超过threshold,就会调用rehash()对Segment中的数组进行扩容

扩容rehash操作:对table扩容

对table扩容成两倍的时候,有些值在数组中的下标未变,有些值会变化为i+capacity,举例如下

原table长度是capacity=4,元素在table中位置

hash值 15  23 34 56 77
在table中下标 3=15%4 3=23%4 2=34%4 0=56%4 1=77%4

扩展一倍之后,通过这个方法,可以快速定位和减少元素重拍的次数

hash值 56   34     77   15,23
下标 0 1 2 3 4 5 6 7
计算 i   i     i+capacity=1+5=5   i+capacity=3+4=7

remove操作

和put方法类似,都是在操作之前要拿到锁,以保证操作的线程安全性

ConcurrentHashMap的弱一致性

遍历链表中是否有形同key以获得value。但是由于遍历过程中,其他线程可能对链表结果做了调整,因此get和containsKey方法返回的可能是已经过时的数据,这一点是ConcurrentHashMap在弱一致性上的体现。如果要求强一致性,那么必须使用Conllections.synchronizedMap()方法

在多线程下避免使用size、containsValue方法

在循环的方法判断两次,每个Segment中所有元素个数的和两次相等才返回值,否则循环次数超过预定义值,就会对Segment进行加锁,影响性能

在1.8下ConcurrentHashMap实现分析

1.8对ConcurrentHashMap的改进
1、取消Segment字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,直接采用table数组元素作为锁,从而实现了缩小锁的粒度,提高了效率

2、原来是table数组+单项链表;现在是table数组+单向链表+红黑树结构(因为如果某一个链表数据过长,单链表查询就必须一个个遍历,效率低,数据多的时候,把链表转换为红黑树,可以提高查询效率,数据少的时候,红黑树会降级为普通链表)

链表使用Node节点,红黑树使用TreeNode节点

图解

核心数据结构和属性

参数:

static final int TREEIFY_THRESHOLD = 8;
将链表转换为红黑树的阈值
static final int UNTREEIFY_THRESHOLD = 6;
判断将红黑树转换成链表的阈值

node

node是最核心的内部类,他包装了key-value键值对

    static class Node<K,V> implements Entry<K,V> {final int hash;final K key;volatile V val;volatile Node<K,V> next;

map本身持有一个node型的数组

transient volatile Node<K,V>[] table;

TreeNode

树节点,当链表长度>=8,就会转换成TreeNode红黑树

    static final class TreeNode<K,V> extends Node<K,V> {TreeNode<K,V> parent;  // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;

与1.8中HashMap不同

1、并不是直接转换为红黑树,而是把这些节点放在TreeBin对象中,有TreeBin完成对红黑树的包装

2、TreeNode扩展自ConcurrentHashMap中Node类,而并非HashMap中的LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针

TreeBin

负责TreeNode节点。他代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap数组中,存放的是TreeBin对象,而不是TreeNode对象,另外这个类还带有了读写锁机制。

    static final class TreeBin<K,V> extends Node<K,V> {TreeNode<K,V> root;volatile TreeNode<K,V> first;volatile Thread waiter;volatile int lockState;// values for lockStatestatic final int WRITER = 1; // set while holding write lockstatic final int WAITER = 2; // set when waiting for write lockstatic final int READER = 4; // increment value for setting read lock

特殊的ForWardingNode

一个特殊的Node节点,hash=-1,其中存储nextTable的引用。有table发生扩容的时候,ForWardingNode发挥作用,作为一个占位符放在table中表示当前节点为null或者已经被移动

    static final class ForwardingNode<K,V> extends Node<K,V> {final Node<K,V>[] nextTable;ForwardingNode(Node<K,V>[] tab) {super(MOVED, null, null, null);this.nextTable = tab;}

sizeCtl:用来控制table的初始化和扩容操作

private transient volatile int sizeCtl;
负数 正在初始化或者扩容操作
-1 正在初始化
-N 代表有N-1个线程正在进行扩容操作
0默认值 代表还没有被初始化
正数 表示初始化大小或者Map中的元素叨叨这个数量是,需要进行扩容了

核心方法

    /*利用硬件级别的原子操作,获得在i位置上的Node节点* Unsafe.getObjectVolatile可以直接获取指定内存的数据,* 保证了每次拿到数据都是最新的*/static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);}/*利用CAS操作设置i位置上的Node节点*/static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}/*利用硬件级别的原子操作,设置在i位置上的Node节点* Unsafe.putObjectVolatile可以直接设定指定内存的数据,* 保证了其他线程访问这个节点时一定可以看到最新的数据*/static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);}

构造方法

可以看到,值是简单的属性设置,并没有初始化table,只有在put、computeIfAbsent、conpute、merge等方法的时候,检查table==null,才开始初始化

    public ConcurrentHashMap18(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 binsinitialCapacity = concurrencyLevel;   // as estimated threadslong size = (long)(1.0 + (long)initialCapacity / loadFactor);int cap = (size >= (long)MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : tableSizeFor((int)size);this.sizeCtl = cap;}

get操作

get方法查找的时候,对于在链表和红黑树上,需要分别去查找

    public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;int h = spread(key.hashCode());/*计算hash值*//*根据hash值确定节点位置*/if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {/*Node数组中的节点就是要找的节点*/if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}/*eh<0 说明这个节点在树上 调用树的find方法寻找*/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;}

put操作

首先根据hash值计算插入点在table中的位置i,如果i位置是空的,还没有存放元素,直接当做链表节点放进去,否则要判断,如果table【i】是红黑树节点,就要按照树的方式插入新的节点,否则把i插入到链表的末尾。

如果是链表,加入元素之后,链表长度大于等于8,就要把链表装换为红黑树。

final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());/*计算hash值*/int binCount = 0;/*死循环 何时插入成功 何时跳出*/for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable();/*如果table为空的话,初始化table*/else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {/*Node数组中的元素,这个位置没有值 ,使用CAS操作放进去*/if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED)/*正在进行扩容,当前线程帮忙扩容*/tab = helpTransfer(tab, f);else {V oldVal = null;/*锁Node数组中的元素,这个位置是Hash冲突组成链表的头结点* 或者是红黑树的根节点*/synchronized (f) {if (tabAt(tab, i) == f) {/*fh>0 说明这个节点是一个链表的节点 不是树的节点*/if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;/*put操作和putIfAbsent操作业务实现*/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) {/*达到临界值8 就需要把链表转换为树结构*/if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}/*Map的元素数量+1,并检查是否需要扩容*/addCount(1L, binCount);return null;}

initTable初始化table

在构造函数中并没有初始化ConcurrentHashMap,初始化时发生在向map中插入元素的时候

    private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {/*小于0表示有其他线程正在进行初始化操作,把当前线程CPU时间让出来。因为对于table的初始化工作,只能有一个线程在进行。*/if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spin/*利用CAS操作把sizectl的值置为-1 表示本线程正在进行初始化*/else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;/*n右移2位本质上就是n变为n原值的1/4,所以* sc=0.75*n */sc = n - (n >>> 2);}} finally {/*将设置成扩容的阈值*/sizeCtl = sc;}break;}}return tab;}

多线程扩容transfer方法

并发扩容,减少扩容带来的时间影响

1、构建一个是原来容量两倍的newTable

2、数据从table复制到newTable

remove

移除方法基本流程和put方法类似,如果是红黑树,会检查容量是不是<=6,是:红黑树就会转换为链表

treeifyBin

用于将过长的链表转换为TreeBin对象。但是他不是直接转换,而是进行容量判断,如果容量没有达到转化的要求。直接返回。与hashmap

不同的是他并没有把TreeNode直接放入红黑树,而是利用了TreeBin这个笑容起来封装素有的TreeNode

size

在扩容和addcount()的时候,size就已经计算好了,需要size会直接返回,这样节省了时间。(jdk7还要实时计算才能够得到size大小)

4.5.2 ConcurrentHashMap的实现分析包含1.7和1.8对比分析相关推荐

  1. gis差值分析_arcgis中七种插值方法的对比分析

    1 地形转栅格工具属于一种插值方法,专门用于创建符合真实地表的数字高程模型 (DEM).该方法基于由 Michael Hutchinson(1988.1989.1996.2000.2011)开发的 A ...

  2. 深度学习中所有的优化器的详细介绍与列表化对比分析

    目录 1. 逐个介绍优化器: ​ 2. 不同优化器方法的宏观对比分析 2.1 在分析中的参数命名 ​ 2.2 不同优化器的列表对比分析 2.3 不同优化器的可视化对比分析 之前对各个优化器理解的不是特 ...

  3. Portal产品对比分析报告

    目录 1概述 2Portal相关产品介绍 2.1商业Portal 2.1.1Bea weblogic portal 2.1.2IBM websphere portal 2.1.3Oracle port ...

  4. 申论作答攻略:对比分析题作答技巧

    在申论考试中,对比分析题是相对较难的题型之一,考生在这一题型上往往失分严重. 1.关系对比分析: 整体表明对象间的关系--深入分析对象间的关系.如让考生分析A和B之间的关系,思路如下:先找出A和B的整 ...

  5. H.265和H.264对比分析(VR视频传输)

    名 称 : H.265和H .264对比分析 姓 名 : 殷松 时 间 : 2017年6月14日 目 录 一. H.265(HEVC)与H.264对比 二.H.265关键技术分析 三.H.265与H. ...

  6. 数据分析法之对比分析法

    数据分析中有很多数据分析的方法,通过这些方法我们能够直接分析出数据中隐藏的有价值的信息,从而得到一个准确的结果.而数据分析方法中,对比分析法是一个十分常用的方法,在这篇文章中我们就详细的为大家介绍一下 ...

  7. 移动三大平台和三大开发模式对比分析

    一:移动三大平台及其对比分析: 1)移动三大平台 2)移动三大平台对比分析 二:三大开发模式及其对比分析: 1)三大开发模式 2)三大开发模式对比分析

  8. FCM聚类与K-means聚类的实现和对比分析

    文章目录 一.收集数据 1.1数据来源 1.2数据描述 二.Fuzzy C-means 2.1FCM介绍 2.2 FCM的原理 2.3 FCM流程 2.4 对于收集的数据集的FCM代码及结果 2.4. ...

  9. 数据分析方法论|利用对比分析有效地说明数据结果和结论

    点击上方蓝字关注我们 对比分析是数据分析中最常用的.最好用.最实用分析方法之一.没有对比就不能说明问题,这也是对比分析在数据分析领域经久不衰的原因之一.对比分析是将两个或两个以上具有可比性的数据进行比 ...

最新文章

  1. C语言:随笔5--指针1
  2. 深入理解Spring的ImportSelector接口
  3. [MIPS汇编语言]InsertionSort插入排序
  4. JDBC_通过DriverManager获得数据库连接
  5. 【视频】vue动态绑定css样式
  6. windows和linux互传文件,用户配置文件和密码配置文件,用户和组管理
  7. 安装用户debian7安装oracle11g
  8. displaytag 相关
  9. C++学习之路 | PTA乙级—— 1025 反转链表 (20分)(精简)
  10. oracle数据库定时同步工具,[每天自动同步一个数据库表的数据]sql server定时同步oracle数据表...
  11. 视频处理简单实例 [OpenCV 笔记2]
  12. hibernate之初学复合主键
  13. 计算机作业有相似度,抄袭检测系统对计算机类电子作业的影响分析
  14. 华为机试HJ20:密码验证合格程序
  15. Winform截图小程序
  16. 阿里:车联网将成新网络入口
  17. Python3中Dict不能在循环中删除元素
  18. 管家婆sql2005数据库一键安装
  19. bootstrap基础表单样式
  20. 通俗易懂的Python入门基础详细教程

热门文章

  1. 因被黑客窃取190GB文件,厄瓜多尔国营电信公司决定“上云”|钛快讯
  2. 苹果手机数据转移到新手机_不同品牌手机一键换机:换新手机怎么转移数据?...
  3. ctfshow 文件包含Web78-79
  4. 第一个多层感知器实例:印第安人糖尿病诊断
  5. 读书郎通过上市聆讯:平板业务毛利率走低,2021年利润同比下滑11%
  6. vue项目中浏览器兼容问题
  7. Eclipse安装阿里巴巴代码插件p3c
  8. 2021年4月自考总结
  9. BUFF 基于区块链技术的虚拟游戏经济平台
  10. 第一款泰泽(Tizen)智能手机在俄罗斯发布