ConcurrentHashMap原理详解(太细了)
Posted 笑我归无处
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ConcurrentHashMap原理详解(太细了)相关的知识,希望对你有一定的参考价值。
一、什么是ConcurrentHashMap
ConcurrentHashMap
和HashMap
一样,是一个存放键值对的容器。使用hash
算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。
同时,ConcurrentHashMap
是线程安全的HashMap
。专门用于多线程环境。
二、ConcurrentHashMap和HashMap以及Hashtable的区别
2.1 HashMap
HashMap
是线程不安全的,因为HashMap
中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用HashMap
是会抛出异常的。
2.2 HashTable
HashTable
是线程安全的,但是HashTable
只是单纯的在put()
方法上加上synchronized
。保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。
2.3 ConcurrentHashMap
ConcurrentHashMap
是线程安全的,ConcurrentHashMap
并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。
由此可见,HashTable
可真是一无是处…
三、ConcurrentHashMap原理
这一节专门介绍ConcurrentHashMap
是如何保证线程安全的。如果想详细了解ConcurrentHashMap
的数据结构,请参考HashMap
。
3.1 volatile修饰的节点数组
请看源码
//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
transient volatile Node<K,V>[] table;
再看看HashMap
是怎么做的
//HashMap没有用volatile修饰节点数组。
transient Node<K,V>[] table;
显然,HashMap
并不是为多线程环境设计的。
3.2 ConcurrentHashMap的put()方法
//put()方法直接调用putVal()方法
public V put(K key, V value)
return putVal(key, value, false);
//所以直接看putVal()方法。
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();
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;
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)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
addCount(1L, binCount);
return null;
我来给大家讲解一下步骤把。
public V put(K key, V value)
首先,put()
方法是没有用synchronized
修饰的。
for (Node<K,V>[] tab = table;;)
新插入一个节点时,首先会进入一个死循环,
情商高的就会说,这是一个乐观锁
进入乐观锁后,
if (tab == null || (n = tab.length) == 0)
tab = initTable();
如果tab
未被初始化,则先将tab
初始化。此时,这轮循环结束,因为被乐观锁锁住,开始下一轮循环。
第二轮循环,此时tab
已经被初始化了,所以跳过。
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
接下来通过key
的hash
值来判断table中是否存在相同的key,如果不存在,执行casTabAt()
方法。
注意,这个操作时不加锁的,看到里面的那行注释了么// no lock when adding to empty bin
。位置为空时不加锁。
这里其实是利用了一个CAS操作。
CAS(Compare-And-Swap):比较并交换
这里就插播一个小知识,CAS就是通过一个原子操作,用预期值去和实际值做对比,如果实际值和预期相同,则做更新操作。
如果预期值和实际不同,我们就认为,其他线程更新了这个值,此时不做更新操作。
而且这整个流程是原子性的,所以只要实际值和预期值相同,就能保证这次更新不会被其他线程影响。
好了,我们继续。
既然这里用了CAS操作去更新值,那么就存在两者情况。
- 实际值和预期值相同
相同时,直接将值插入,因为此时是线程安全的。好了,这时插入操作完成。使用break;
跳出了乐观锁。循环结束。 - 实际值和预期值不同
不同时,不进行操作,因为此时这个值已经被其他线程修改过了,此时这轮操作就结束了,因为还被乐观锁锁住,进入第三轮循环。
第三轮循环中,前面的判断又会重新执行一次,我就跳过不说了,进入后面的判断。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
这里判断的是tab
的状态,MOVED
表示在扩容中,如果在扩容中,帮助其扩容。帮助完了后就会进行第四轮循环。
终于,来到了最后一轮循环。
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)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
上面的判断都不满足时,就会进入最后的分支,这条分支表示,key
的hash
值位置不为null
(之前的判断是hash
值为null
时直接做插入操作),表示发生了hash
冲突,此时节点就要通过链表的形式存储这个插入的新值。Node
类是有next
字段的,用来指向链表的下一个位置,新节点就往这插。
synchronized (f)
看,终于加排它锁了,只有在发生hash冲突的时候才加了排它锁。
if (tabAt(tab, i) == f)
if (fh >= 0)
重新判断当前节点是不是第二轮判断过的节点,如果不是,表示节点被其他线程改过了,进入下一轮循环,
如果是,再次判断是否在扩容中,如果是,进入下一轮循环,
如果不是,其他线程没改过,继续走,
for (Node<K,V> e = f;; ++binCount)
for循环,循环遍历这个节点上的链表,
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek))))
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
找到一个hash值相同,且key也完全相同的节点,更新这个节点。
如果找不到
if ((e = e.next) == null)
pred.next = new Node<K,V>(hash, key,
value, null);
break;
往链表最后插入这个新节点。因为在排他锁中,这些操作都可以直接操作。终于到这插入就基本完成了。
总结
做插入操作时,首先进入乐观锁,
然后,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器,
如果已经初始化,则判断该hash
位置的节点是否为空,如果为空,则通过CAS操作进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。
如果没有扩容,则进行最后一步,先加锁,然后找到hash
值相同的那个节点(hash冲突),
循环判断这个节点上的链表,决定做覆盖操作还是插入操作。
循环结束,插入完毕。
3.3 ConcurrentHashMap的get()方法
//ConcurrentHashMap的get()方法是不加锁的,方法内部也没加锁。
public V get(Object key)
看上面这代码,ConcurrentHashMap
的get()
方法是不加锁的,为什么可以不加锁?因为table
有volatile
关键字修饰,保证每次获取值都是最新的。
//Hashtable的get()是加锁的,所以性能差。
public synchronized V get(Object key)
再看看Hashtable
,差距啊。
四、使用场景
嗯,多线程环境下,更新少,查询多时使用的话,性能比较高。
乐观锁嘛,认为更新操作时不会被其他线程影响。所以时候再更新少的情况下性能高。
对你有帮助吗?点个赞吧~
不愧是阿里“扫地僧”内部“SpringCloudAlibaba学习笔记”这细节讲解太细了!
SpringCloud Alibaba 为什么会出现?
Spring Cloud Netflix 项目进入维护模式,Spring Cloud Netflix 将不再开发新的组件,我们知道Spring Cloud 版本迭代算是比较快的,因而出现了很多中岛的 ISSUE 都来不及 Fix 就又推另一个 Release 了 。进入维护模式意思就是目前已知以后一段时间 Spring Cloud Netflix 提供的服务和功能就这么多了, 不再开发性的组件和功能了。 以后将以维护和 Merge 分支 Full Requset 为主。换句话说:就是SpringCloud的技术栈不再完整了!此时,我们就有必要寻找一个新的完整的技术栈。
SpringCloud Alibaba 什么是?
Spring-Cloud-Alibaba项目由阿里巴巴的开源组件和多个阿里云产品组成,旨在实现和公开众所周知的Spring框架模式和抽象,为使用阿里巴巴产品的Java开发者带来Spring-Boot和Spring-Cloud的好处。
SpringCloud Alibaba 能干什么?
- 服务限流降级:默认支持 Servlet、Feign\\
RestTemplate、Dubbo、和RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级骨子额,还支持查看限流降级 Metrics 控制。 - 服务注册于发现:适配 Spring Cloud 服务注册于发现标准,默认集成 Ribbon 支持
- 分布式配置管理:支持分布式系统中的外部话配置,配置更改时自动刷新。
- 消息驱动能力:基于Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用,任何时间、任何低调存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务,网格任务支持海量任务均匀分配到所有 Worker (schedulerx-client) 执行。
为什么要学习SpringCloud Alibaba
Spring Cloud Alibaba为分布式应用开发提供了一站式解决方案。它包含开发分布式应用程序所需的所有组件,可以轻松地使用Spring Cloud开发应用程序。
使用Spring Cloud Alibaba,只需添加一些注解和少量配置,即可将Spring Cloud应用连接到Alibaba的分布式解决方案中,并使用Alibaba中间件构建分布式应用系统。
正是基于这些原因,我们有必要来学习SpringCloud Alibaba技术。
那如何学习呢?市面上对于SpringCloud Alibaba讲解的资料零零碎碎,根本不成完整体系;去官网学习又无从下手,饱受打击。因此我将在这分享我精心收集整理的《SpringCloudAlibaba学习笔记》从入门到入魂
此笔由阿里大佬编写只流传于内部,几经波折终于到手,我已看完小半部分,确实不错特意在此分享,回馈小伙伴。这份笔记究竟写了些什么?下面我们一起来看看
Spring-Cloud-Alibaba脑图
Ps:由于内容较多,本次将展示部分,如果看得不过瘾想更加深入地了解本笔记彻底掌握S pring Cloud Alibaba可在文末了解详情。
模块一 微服务架构设计: 本模块主要介绍了什么是微服务体系结构,以及微服务体系结构设计中的一些常见问题。
模块二 Nacos 服务治理:Nacos注册中心是整个微服务体系结构的核心。本文将详细介绍Nacos的安装、使用和集群构建过程,并以图文的形式介绍Nacos服务发现的基本原理。
模块三 系统保护:Sentinel是Alibaba提供的服务保护中间件。使用sentinel可以有效地防止分布式体系结构的系统崩溃。在此阶段,我们将解释Sentinel在限流、熔断、代码控制等方面的最佳实践。
模块四 高级特性:在这一阶段,我们将介绍SpringCloudAlibaba提供的许多高级功能。例如:配置中心、链路跟踪、性能监控、分布式事务、消息队列等。我们将从应用介绍到原理分析,逐一讲解这些技术。
模块五 微服务通信:当服务需要相互通信时,springcloudAlibaba支持RPC和restful解决方案。相应的产品是Dubbo和openfeign。在这个阶段,我将给出这些组件的最佳实践和原理分析。
模块六 微服务架构最佳实践:这阶段,我将拿出自己的私藏干货,为大家讲解微服务架构的综合应用和项目实践。在这里我们将接触到Seata分布式事务架构、多级缓存设计、老项目升级策略!
总结
Spring Cloud Netflix 项目进入维护模式,将不再开发新的组件,SpringCloud性能上不 能满足互联企业的发展需求。但互联网发展又迫切需要解决微服务的方案,因此龙头企业阿里应运而生推出了Spring Cloud Alibaba新一代的微服务架构解决方案。
如果你还没有掌握这套主流技术,现在想要在最短的时间里吃透它,一键三连(点赞+收藏+关注)来获取这套完整的体系资料。
以上是关于ConcurrentHashMap原理详解(太细了)的主要内容,如果未能解决你的问题,请参考以下文章
Android开发面试:requestLayout() 这么问,面试者直呼:太细了!
写的太细了!Spring MVC拦截器的应用,建议收藏再看!