【并发编程】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的区别进行讲解。

相关推荐
码熔burning2 分钟前
JVM 面试精选 20 题(续)
jvm·面试·职场和发展
Victor3568 分钟前
Redis(14)Redis的列表(List)类型有哪些常用命令?
后端
Victor3568 分钟前
Redis(15)Redis的集合(Set)类型有哪些常用命令?
后端
卷福同学9 分钟前
来上海三个月,我在马路边上遇到了阿里前同事...
java·后端
bobz9659 小时前
小语言模型是真正的未来
后端
DevYK9 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent
艾伦~耶格尔10 小时前
【集合框架LinkedList底层添加元素机制】
java·开发语言·学习·面试
一只叫煤球的猫10 小时前
🕰 一个案例带你彻底搞懂延迟双删
java·后端·面试
冒泡的肥皂10 小时前
MVCC初学demo(一
数据库·后端·mysql