ConcurrentHashMap原理详解(太细了)

Posted 笑我归无处

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ConcurrentHashMap原理详解(太细了)相关的知识,希望对你有一定的参考价值。

一、什么是ConcurrentHashMap

ConcurrentHashMapHashMap一样,是一个存放键值对的容器。使用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

接下来通过keyhash值来判断table中是否存在相同的key,如果不存在,执行casTabAt()方法。
注意,这个操作时不加锁的,看到里面的那行注释了么// no lock when adding to empty bin。位置为空时不加锁。
这里其实是利用了一个CAS操作。

CAS(Compare-And-Swap):比较并交换

这里就插播一个小知识,CAS就是通过一个原子操作,用预期值去和实际值做对比,如果实际值和预期相同,则做更新操作。
如果预期值和实际不同,我们就认为,其他线程更新了这个值,此时不做更新操作。
而且这整个流程是原子性的,所以只要实际值和预期值相同,就能保证这次更新不会被其他线程影响。

好了,我们继续。
既然这里用了CAS操作去更新值,那么就存在两者情况。

  1. 实际值和预期值相同
    相同时,直接将值插入,因为此时是线程安全的。好了,这时插入操作完成。使用break;跳出了乐观锁。循环结束。
  2. 实际值和预期值不同
    不同时,不进行操作,因为此时这个值已经被其他线程修改过了,此时这轮操作就结束了,因为还被乐观锁锁住,进入第三轮循环。

第三轮循环中,前面的判断又会重新执行一次,我就跳过不说了,进入后面的判断。

 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;
    

上面的判断都不满足时,就会进入最后的分支,这条分支表示,keyhash值位置不为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)

看上面这代码,ConcurrentHashMapget()方法是不加锁的,为什么可以不加锁?因为tablevolatile关键字修饰,保证每次获取值都是最新的。

//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学习笔记》从入门到入魂

此笔由阿里大佬编写只流传于内部,几经波折终于到手,我已看完小半部分,确实不错特意在此分享,回馈小伙伴。这份笔记究竟写了些什么?下面我们一起来看看

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

Spring-Cloud-Alibaba脑图

Ps:由于内容较多,本次将展示部分,如果看得不过瘾想更加深入地了解本笔记彻底掌握S pring Cloud Alibaba可在文末了解详情

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

模块一 微服务架构设计: 本模块主要介绍了什么是微服务体系结构,以及微服务体系结构设计中的一些常见问题。

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

模块二 Nacos 服务治理:Nacos注册中心是整个微服务体系结构的核心。本文将详细介绍Nacos的安装、使用和集群构建过程,并以图文的形式介绍Nacos服务发现的基本原理。

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

模块三 系统保护:Sentinel是Alibaba提供的服务保护中间件。使用sentinel可以有效地防止分布式体系结构的系统崩溃。在此阶段,我们将解释Sentinel在限流、熔断、代码控制等方面的最佳实践。

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

模块四 高级特性:在这一阶段,我们将介绍SpringCloudAlibaba提供的许多高级功能。例如:配置中心、链路跟踪、性能监控、分布式事务、消息队列等。我们将从应用介绍到原理分析,逐一讲解这些技术。

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

模块五 微服务通信:当服务需要相互通信时,springcloudAlibaba支持RPC和restful解决方案。相应的产品是Dubbo和openfeign。在这个阶段,我将给出这些组件的最佳实践和原理分析。

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

模块六 微服务架构最佳实践:这阶段,我将拿出自己的私藏干货,为大家讲解微服务架构的综合应用和项目实践。在这里我们将接触到Seata分布式事务架构、多级缓存设计、老项目升级策略!

不愧是阿里内部“SpringCloudAlibaba学习笔记”这细节讲解,神了

总结

Spring Cloud Netflix 项目进入维护模式,将不再开发新的组件,SpringCloud性能上不 能满足互联企业的发展需求。但互联网发展又迫切需要解决微服务的方案,因此龙头企业阿里应运而生推出了Spring Cloud Alibaba新一代的微服务架构解决方案。

如果你还没有掌握这套主流技术,现在想要在最短的时间里吃透它,一键三连(点赞+收藏+关注)来获取这套完整的体系资料。

以上是关于ConcurrentHashMap原理详解(太细了)的主要内容,如果未能解决你的问题,请参考以下文章

Android开发面试:requestLayout() 这么问,面试者直呼:太细了!

写的太细了!Spring MVC拦截器的应用,建议收藏再看!

淘宝太细了:mysql 和 es 的5个一致性方案,你知道吗?

ConcurrentHashMap原理详解

ConcurrentHashMap源码分析(1.8)

ConcurrentHashMap(JDK1.8)为什么要放弃Segment