1【理解】

ConcurrentHashMap:检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和remove)重叠,ConcurrentHashMap跟Hashtable类似但不同于HashMap,它不可以存放空值,key和value都不可以为null【null值用来判断是否需要加锁立即重试】。

ConcurrentHashMap从JDK1.5开始随java.util.concurrent包一起引入JDK中,在JDK8以前,ConcurrentHashMap都是基于Segment分段锁来实现的,在JDK8以后,就换成synchronized和CAS这套实现机制了

JDK1.8中的ConcurrentHashMap中仍然存在Segment这个类,而这个类的声明则是为了兼容之前的版本序列化而存在的。

JDK1.8中的ConcurrentHashMap不再使用Segment分段锁,而是以table数组的头结点作为synchronized的锁。和JDK1.8中的HashMap类似,对于hashCode相同的时候,在Node节点的数量少于8个时,这时的Node存储结构是链表形式,时间复杂度为O(N),当Node节点的个数超过8个时,则会转换为红黑树,此时访问的时间复杂度为O(long(N))。

2 【保证线程安全】

● 一、使用volatile保证当Node中的值变化时对于其他线程是可见的

【Node中的val和next都被volatile关键字修饰。我们改动val的值或者next的值对于其他线程是可见的,因为volatile关键字,会在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。】

【ConcurrentHashMap提供类似tabAt来读取Table数组中的元素,这里是以volatile读的方式读取table数组中的元素,主要通过Unsafe这个类来实现的,保证其他线程改变了这个数组中的值的情况下,在当前线程get的时候能拿到。】【】

【而与之对应的,是setTabAt,这里是以volatile写的方式往数组写入元素,这样能保证修改后能对其他线程可见。】

 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);}static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);}

3 【使用table数组的头结点作为synchronized的锁来保证写操作的安全】

【当头结点不为null时,则使用该头结点加锁,这样就能多线程去put hashCode相同的时候不会出现数据丢失的问题。synchronized是互斥锁,有且只有一个线程能够拿到这个锁,从而保证了put操作是线程安全的。】

4 【当头结点为null时,使用CAS操作来保证数据能正确的写入】

    final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());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();//当头结点为null,则通过casTabAt方式写入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}else if ((fh = f.hash) == MOVED)//正在扩容tab = helpTransfer(tab, f);else {V oldVal = null;//头结点不为null,使用synchronized加锁synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {//此时hash桶是链表结构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;}}else if (f instanceof ReservationNode)throw new IllegalStateException("Recursive update");}}if (binCount != 0) {//当链表结构大于等于8,则将链表转换为红黑树if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;}

【所谓的CAS,即compareAndSwap,执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。】

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);}

【 asTabAt同样是通过调用Unsafe类来实现的,调用Unsafe的compareAndSwapObject来实现,其实如果仔细去追踪这条线路,会发现其实最终调用的是cmpxchg这个CPU指令来实现的,这是一个CPU的原子指令,能保证数据的一致性问题。】

1.8和之前的版本比较  参考 【ConcurrentHashMap 1.7】 理解性知识整 https://blog.csdn.net/qfzhangwei/article/details/101665928

旧版本的一个segment锁,保护了多个hash桶,而jdk8版本的一个锁只保护一个hash桶,由于锁的粒度变小了,写操作的并发性得到了极大的提升。

【更多的扩容线程】

扩容时,需要锁的保护。因此:旧版本最多可以同时扩容的线程数是segment锁的个数。

而jdk8的版本,理论上最多可以同时扩容的线程数是:hash桶的个数(table数组的长度)。但是为了防止扩容线程过多,ConcurrentHashMap规定了扩容线程每次最少迁移16个hash桶,因此jdk8的版本实际上最多可以同时扩容的线程数是:hash桶的个数/16,每个线程至少迁移16个桶。

【扩容期间,依然保证较高的并发度】

旧版本的segment锁,锁定范围太大,导致扩容期间,写并发度,严重下降。

而新版本的采用更加细粒度的hash桶级别锁,扩容期间,依然可以保证写操作的并发度。

【ConcurrentHashMap的重要结构与方法】

ConcurrentHashMap内部,和hashmap一样,维护了一个table数组,数组元素是Node链表或者红黑树.

【关于table数组,有3个重要方法】

//以volatile读的方式读取table数组中的元素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);}//以volatile写的方式,将元素插入table数组static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);}//以CAS的方式,将元素插入table数组static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {//原子的执行如下逻辑:如果tab[i]==c,则设置tab[i]=v,并返回ture.否则返回falsereturn U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}

旧版本对table数组元素的读写,都是在segment锁保护的情况下进行的,因此不会内存可见性问题。而jdk8的实现中,锁的粒度是hash桶,因此对table数组元素的读写,大部分都是在没有锁的保护下进行的,那么该如何保证table数组元素的内存可见性?【线程重试性代价小于----线程上下文交换的代价】 volatile

ConcurrentHashMap中的锁是hash桶的头结点,那么当多个put线程访问头结点为空的hash桶时,在没有互斥锁保护的情况下,多个put线程都会尝试将元素插入头结点,此时如何确保并发安全呢?  CAS

6 【应用】

在哪里用的?
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);}然后,我们看下ConcurrentHashMap的put方法是如何通过CAS确保线程安全的:
假设此时有2个put线程,都发现此时桶为空,线程一执行casTabAt(tab,i,null,node1),此时tab[i]等于预期值null,因此会插入node1。随后线程二执行casTabAt(tba,i,null,node2),此时tab[i]不等于预期值null,插入失败。然后线程二会回到for循环开始处,重新获取tab[i]作为预期值,重复上述逻辑。final V putVal(K key, V value, boolean onlyIfAbsent) {...for (Node<K,V>[] tab = table;;) {...//key定位到的hash桶为空if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//cas设置tab[i]的头结点。if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;   //设置成功,跳出for循环//设置失败,说明tab[i]已经被另一个线程修改了。回到for循环开始处,重新判断hash桶是否为空。如何往复,直到设置成功,或者hash桶不空。}else{synchronized (f) {//}}}...}
CAS的其他应用
//JDK7版本的 AtomicInteger 类的原子自增操作public final int getAndIncrement() {for (;;) {//获取valueint current = get();int next = current + 1;//value值没有变,说明其他线程没有自增过,将value设置为nextif (compareAndSet(current, next))return current;//否则说明value值已经改变,回到循环开始处,重新获取value。}}get方法
get方法同样利用了volatile特性,实现了无锁读。
查找value的过程如下:
1. 根据key定位hash桶,通过tabAt的volatile读,获取hash桶的头结点。
2. 通过头结点Node的volatile属性next,遍历Node链表
3. 找到目标node后,读取Node的volatile属性val
可见上述3个操作都是volatile读,因此可以做到在不加锁的情况下,保证value的内存可见性public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;int h = spread(key.hashCode());if ((tab = table) != null && (n = tab.length) > 0 &&//定位目标hash桶,通过tabAt方法valatile读,读取hash桶的头结点(e = tabAt(tab, (n - 1) & h)) != null) {//第一个节点就是要找的元素if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))//e.val也是valatilereturn e.val;}//特殊节点(红黑树,已经迁移的节点(ForwardingNode)等else if (eh < 0)return (p = e.find(h, key)) != null ? p.val : null;//遍历node链表(e.next也是valitle变量)while ((e = e.next) != null) {if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;}put方法
2. 由于锁的粒度是hash桶,多个put线程只有在请求同一个hash桶时,才会被阻塞。请求不同hash桶的put线程,可以并发执行。
4. put线程,请求的hash桶为空时,采用for循环+CAS的方式无锁插入。remove方法
如图所示:删除的node节点的next依然指着下一个元素。此时若有一个遍历线程正在遍历这个已经删除的节点,这个遍历线程依然可以通过next属性访问下一个元素。从遍历线程的角度看,他并没有感知到此节点已经删除了,这说明了ConcurrentHashMap提供了弱一致性的迭代器。遍历操作可以参考ConcurrentHashMap源码分析(JDK8) 遍历操作分析public V remove(Object key) {return replaceNode(key, null, null);}/**参数value:当 value==null 时 ,删除节点 。否则 更新节点的值为value参数cv:一个期望值, 当 map[key].value 等于期望值cv  或者 cv==null的时候 ,删除节点,或者更新节点的值*/final V replaceNode(Object key, V value, Object cv) {int hash = spread(key.hashCode());for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;//table还没有初始化或者key对应的hash桶为空if (tab == null || (n = tab.length) == 0 ||(f = tabAt(tab, i = (n - 1) & hash)) == null)break;//正在扩容else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {V oldVal = null;boolean validated = false;synchronized (f) {//cas获取tab[i],如果此时tab[i]!=f,说明其他线程修改了tab[i]。回到for循环开始处,重新执行if (tabAt(tab, i) == f) {//node链表if (fh >= 0) {validated = true;for (Node<K,V> e = f, pred = null;;) {K ek;//找的key对应的nodeif (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {V ev = e.val;//cv参数代表期望值//cv==null:表示直接更新value/删除节点//cv不为空,则只有在key的oldValue等于期望值的时候,才更新value/删除节点//符合更新value或者删除节点的条件if (cv == null || cv == ev ||(ev != null && cv.equals(ev))) {oldVal = ev;//更新valueif (value != null)e.val = value;//删除非头节点else if (pred != null)pred.next = e.next;//删除头节点else//因为已经获取了头结点锁,所以此时不需要使用casTabAtsetTabAt(tab, i, e.next);}break;}//当前节点不是目标节点,继续遍历下一个节点pred = e;if ((e = e.next) == null)//到达链表尾部,依旧没有找到,跳出循环break;}}//红黑树else if (f instanceof TreeBin) {validated = true;TreeBin<K,V> t = (TreeBin<K,V>)f;TreeNode<K,V> r, p;if ((r = t.root) != null &&(p = r.findTreeNode(hash, key, null)) != null) {V pv = p.val;if (cv == null || cv == pv ||(pv != null && cv.equals(pv))) {oldVal = pv;if (value != null)p.val = value;else if (t.removeTreeNode(p))setTabAt(tab, i, untreeify(t.first));}}}}}if (validated) {if (oldVal != null) {//如果删除了节点,更新sizeif (value == null)addCount(-1L, -1);return oldVal;}break;}}}return null;}

7 【图示】

【ConcurrentHashMap 1.8】 理解性知识整理相关推荐

  1. pmp-隐性知识显性知识

    pmp-隐性知识&显性知识 隐性知识 隐性知识是存在于人们的头脑或形体语言中的,职能意会无法言传的知识.隐性知识包含两个层面,一是"技术"层面的非正式和难以言表的" ...

  2. 深入理解计算机系统 -资料整理 高清中文版_在所不辞的博客-CSDN博客_深入理解计算机系统第四版pdf

    深入理解计算机系统 -资料整理 高清中文版_在所不辞的博客-CSDN博客_深入理解计算机系统第四版pdf

  3. 理论与哲学就是梳理无限感性经验和知性知识的工具

    理论与哲学就是梳理无限感性经验和知性知识的工具, 是因为人脑的特性,或者说人脑功能的局限性而被人创造出来的思想工具. 工具服务于实践,并被实践所检验(描述.解释.预见). 工具一旦创建就有其客观性,但 ...

  4. 机器阅读理解MRC论文整理

    机器阅读理解MRC论文整理 最近发现一篇机器阅读理解整理的博客机器阅读理解整理整理于2020年 论文代码查找网站: https://dblp.uni-trier.de/db/conf/acl/acl2 ...

  5. 深入理解JVM(整理)

    1.JVM概念 运行Java程序的虚拟计算机. 是Java语言的运行环境(Runtime). 2.内存区域与内存溢出 2.1概述 JVM自动内容管理机制,避免了JAVA程序员编写对象的delete/f ...

  6. 关于数字证书理解的简单整理以及12306站点证书简单分析

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/sundacheng1989/article/details/25540601 首先简单理解一下什么是 ...

  7. Effulgent的《深入理解Direct3D9》整理版(转)

    深入理解Direct3D9 深入理解D3D9对图形程序员来说意义重大,我把以前的一些学习笔记都汇总起来,希望对朋友们有些所帮助,因为是零散笔记,思路很杂,还请包涵. 其实只要你能完美理解D3DLOCK ...

  8. 《Restricted Boltzmann Machines for Collaborative Filtering》理解与资料整理

    1 原文如下 2 理解 这篇文章尝试把RBM应用到协同过滤中,在netflix上的数据集做实验,RBM方法与SVD方法线性插值相结合,能提高系统性能6%左右. 2.1问题描述: 对电影的推荐,用户对电 ...

  9. 对软件开发中uml建模的理解和图形整理(一)

    由于uml(统一建模语言)在开发中经常会用到,特别是在软件开发中的OOAD阶段,因此要理解和使用uml显得尤为重要.在uml开始之前,咱先回顾一个OOAD.OOP的主要特征. OOAD:根据面向对象的 ...

最新文章

  1. 移动4G打造排污视频监控系统助力咸宁环保建设
  2. 各种编程语言功能综合简要介绍(C,C++,JAVA,PHP,PYTHON,易语言)
  3. 创建多级目录函数MakeSureDirectoryPathExists()所需头文件
  4. 机器学习物语(2):大数定理军团
  5. 批量绑定(bulk binds):FOR循环与FORALL的性能比较
  6. iis7 上传限制问题
  7. [Swift]LeetCode382. 链表随机节点 | Linked List Random Node
  8. 使用github+hexo搭建静态blog
  9. mysql mha配置idrac远程关机_iDRAC远程管理功能试用_戴尔 PowerEdge R810(Xeon E7520/16GB/3*146GB)_服务器评测与技术-中关村在线...
  10. 查看linux cpu负载均衡,关于linux内核cpu进程的负载均衡
  11. Java基础小常识-final,抽象类,接口-(11)
  12. USB3.0剖析(锆石科技FPGA)
  13. .bat批处理命令的介绍
  14. 计算机网络技术有关的心得体会,计算机网络技术学习心得体会
  15. 网络安全技术 3.28 作业
  16. hcia是什么等级的证书_HCIA是什么等级的证书
  17. java8 map_Java8 Map 示例:一个略复杂的数据映射聚合例子及代码重构
  18. 变态杀人狂(约瑟夫环问题)
  19. selenium获取某网站工作岗位信息(含火狐浏览器的驱动下载安装步骤)
  20. 并发、并行、同步、异步、进程,线程、串行、并行?一文弄懂八大概念

热门文章

  1. 喷墨机关键部件原理详解及故障修复
  2. 怎么把windows改成linux系统,如何将linux系统更换成windows系统
  3. Word目录标题中含英文对齐设置问题(含操作视频)
  4. 【全网最全】自用整理的所有版本的EDEM-FLUENT耦合欧拉接口
  5. qrobot开发总结之android手势识别
  6. 2018 东华软件股份有限公司 ETL开发工程师笔试题
  7. 【dede】安装完CMS源码后,会发现访问网站首页是报错 /templets/default/index.htm Not Found!
  8. eclipse c 调用java_eclipse项目,用命令行执行javac,java
  9. (二)Redis基本操作-List
  10. 国内k8s集群部署的几种方式