【并发编程】ConcurrentHashMap底层结构和原理

📫作者简介:小明Java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化,文章内容兼具广度、深度、大厂技术方案,对待技术喜欢推理加验证,就职于知名金融公司后端高级工程师。

🏆 2022博客之星TOP3 | CSDN博客专家 | 后端领域优质创作者 | CSDN内容合伙人

🏆 InfoQ(极客邦)签约作者、阿里云专家 | 签约博主、51CTO专家 | TOP红人、华为云享专家

🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~

本文目录

本文导读

一、ConcurrentHashMap底层实现(JDK1.8)

1、put() 方法源码

2、get() 方法源码

二、ConcurrentHashMap底层实现(JDK1.7)

三、ConcurrentHashMap的jdk7和8的区别

四、ConcurrentHashMap与Hashtable的区别

总结

本文导读

本文JDK1.8中ConcurrentHashMap底层实现以及put、get源码,对常见面试题ConcurrentHashMap的jdk7和8的区别、ConcurrentHashMap与Hashtable的区别进行讲解。

一、ConcurrentHashMap底层实现(JDK1.8)

底层数据结构:Node数组 + 红黑树

保证线程安全的方式:乐观锁 + Sysnchronized(1.8中的分段其实就是table数组中一个个的hash槽,这样使得添加节点时加锁粒度更小,并发度也更高)

Sysnchronized 锁 : 锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁)并发扩容

读操作是无锁:Node 的 val 和 next 使用 volatile 修饰,读写线程对该变量互相可见,数组用volatile修饰,保证扩容时被读线程感知。

arduino 复制代码
/**
* 每个 Node 里面是 key-value 的形式
* 并且把 value 用 volatile 修饰,以便保证可见性
* 同时内部还有一个指向下一个节点的 next 指针,方便产生链表结构
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
// ...
 }

为什么在有Synchronized 的情况下还要使用CAS

因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能。

1、put() 方法源码

final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) { throw new NullPointerException(); }

ini 复制代码
//计算 hash 值
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();
    }

    // 找该 hash 值对应的数组下标
    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        //如果该位置是空的,就用 CAS 的方式放入新值
        if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null))) {
            break;
        }
    }

    //hash值等于 MOVED 代表在扩容
    else if ((fh = f.hash) == MOVED) {
        tab = helpTransfer(tab, f);
    }

    //槽点上是有值的情况
    else {
        V oldVal = null;
        //用 synchronized 锁住当前槽点,保证并发安全
        synchronized (f) {
            if (tabAt(tab, i) == f) {

                //如果是链表的形式
                if (fh >= 0) {
                    binCount = 1;
                    //遍历链表
                    for (Node<K, V> e = f; ; ++binCount) {
                        K ek;
                        //如果发现该 key 已存在,就判断是否需要进行覆盖,然后返回
                        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;
                        //到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后
                        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;

                    //调用 putTreeVal 方法往红黑树里增加数据
                    if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) {
                        oldVal = p.val;
                        if (!onlyIfAbsent) {
                            p.val = value;
                        }
                    }
                }
            }
        }

        if (binCount != 0) {
            //检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值是 8
            if (binCount >= TREEIFY_THRESHOLD) {
                treeifyBin(tab, i);
            }
            //putVal 的返回是添加前的旧值,所以返回 oldVal
            if (oldVal != null) {
                return oldVal;
            }
            break;
        }
    }
}
addCount(1L, binCount);
return null;
 }

2、get() 方法源码

kotlin 复制代码
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算 hash 值
int h = spread(key.hashCode());

//如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null
if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {

    //判断头结点是否就是我们需要的节点,如果是则直接返回
    if ((eh = e.hash) == h) {
        if ((ek = e.key) == key || (ek != null && key.equals(ek)))
            return e.val;
    }

    //如果头结点 hash 值小于 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;
}

二、ConcurrentHashMap底层实现(JDK1.7)

底层数据结构:Segment分段(数组+链表)

在jdk7中ConcurrentHashMap内部进行了Segment分段,Segment继承了ReentrantLock,各个Segment之间都是相互独立上锁的,互不影响。

每个Segment的底层数据结构与HashMap类似,是数组+链表。默认有0~15共16个Segment,所以最多可以同时支持16个线程并发操作(操作分别分布在不同的Segment上)。

16这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

相比于之前的Hashtable每次操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁。

三、ConcurrentHashMap的jdk7和8的区别

数据结构:Java7采用Segment分段锁来实现,Java8中的ConcurrentHashMap使用数组+链表+红黑树

并发度:Java7中,每个Segment独立加锁,最大并发个数就是Segment的个数,默认是16。Java8中,锁粒度是table数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高。

原理:Java7采用Segment分段锁来保证安全,而Segment是继承自ReentrantLock。Java8中放弃了Segment的设计,采用Node+CAS+synchronized保证线程安全。

Hash冲突:Java7链表;Java8链表转换为红黑树,来提高查找效率。

查询时间复杂度:Java7遍历链表的时间复杂度是O(n),n为链表长度。Java8如果变成遍历红黑树,那么时间复杂度降低为O(log(n)),n为树的节点个数。

四、ConcurrentHashMap与Hashtable的区别

版本:ConcurrentHashMap(JDK1.5)与Hashtable(JDK1.0)

实现线程安全的方式不同:Hashtable的方法是被synchronized关键字修饰的。而ConcurrentHashMap利用了CAS+synchronized+Node节点的方式

性能不同:当线程数量增加的时候,Hashtable的性能会急剧下降,因为每一次修改都需要锁住整个对象,而其他线程在此期间是不能操作的,还会带来额外的上下文切换等开销,所以此时它的吞吐量甚至还不如单线程的情况。

在ConcurrentHashMap中,就算上锁也仅仅会对一部分上锁而不是全部都上锁,所以多线程中的吞吐量通常都会大于单线程的情况。

迭代时修改:Hashtable(包括HashMap)不允许在迭代期间修改内容,否则会抛出ConcurrentModificationException异常,其原理是检测modCount变量(当前Hashtable被修改的次数)。每一次去调用Hashtable的包括addEntry()、remove()、rehash()等方法中,都会修改modCount的值。迭代器在进行next的时候,也可以感知到,于是它就会发现modCount不等于expectedModCount。

总结

本文JDK1.8中ConcurrentHashMap底层实现以及put、get源码,对常见面试题ConcurrentHashMap的jdk7和8的区别、ConcurrentHashMap与Hashtable的区别进行讲解。

相关推荐
海绵波波1074 小时前
flask后端开发(10):问答平台项目结构搭建
后端·python·flask
网络风云5 小时前
【魅力golang】之-反射
开发语言·后端·golang
Q_19284999065 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
breaksoftware6 小时前
低代码开源项目Joget的研究——Joget7社区版安装部署
低代码·开源
运维&陈同学6 小时前
【Kibana01】企业级日志分析系统ELK之Kibana的安装与介绍
运维·后端·elk·elasticsearch·云原生·自动化·kibana·日志收集
测试界萧萧7 小时前
15:00面试,15:08就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
先生先生3937 小时前
Java小公司面试
面试
小天努力学java7 小时前
【面试系列】深入浅出 Spring
java·spring·面试
笑呵呵的大文子8 小时前
论文分享—— 软件物料清单(SBOM)开源与专有工具的现状研究
开源·sbom
-$_$-8 小时前
【LeetCode 面试经典150题】详细题解之哈希表篇
leetcode·面试·散列表