掘金 后端 ( ) • 2024-05-13 10:28

theme: orange highlight: a11y-dark

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~


🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

在并发编程中,线程安全是一个非常重要的概念。而在Java中,ConcurrentHashMap是一个经典的线程安全的容器,其在多线程并发访问时性能表现优异。所以此文,我就给大家来聊聊它,其中它也是热门面试题八股文之一呢。

摘要

本文将从源代码解析、应用场景案例、优缺点分析等多方面,对Java中的ConcurrentHashMap进行全面深入地探讨和分析,就差手把手带着同学们捋一遍了。

ConcurrentHashMap解读

概述

首先,ConcurrentHashMap是Java中的一个线程安全的哈希表实现。它 在 JDK 1.7 和 JDK 1.8 中的实现有显著的不同,这些差异主要体现在它们的内部结构和并发控制机制上。

结构

对于JDK1.7而言:

在 JDK 1.7 中,ConcurrentHashMap 使用了分段锁(Segment)的机制。这种设计允许多个线程同时对不同的段(Segment)进行操作,从而提高了并发性能。每个段实际上是一个独立的 HashEntry 数组,它有自己的锁。当一个线程需要修改一个段时,它将获得该段的锁,而其他线程仍然可以对其他段进行读取操作。

特点:

  • 分段锁提供了一定程度的并发性,但锁的粒度相对较大。
  • 每个段是一个独立的小型哈希表,有自己的扩容机制。
  • 性能受限于段的数量和大小,以及锁的竞争。

对于JDK1.8而言:

JDK 1.8 完全重写了 ConcurrentHashMap 的实现,放弃了分段锁机制,转而使用一种基于 CAS(Compare-And-Swap)和原子变量的锁机制。在 JDK 1.8 中,ConcurrentHashMap 使用了一个数组加上链表(在链表过长时转换为红黑树)的数据结构。这种设计使得 ConcurrentHashMap 能够在高并发场景下提供更好的性能。

特点

  • 使用了更细粒度的锁,每个链表节点(或红黑树节点)都有自己的锁,这被称为锁分离(lock striping)。
  • 通过使用红黑树来优化长链表的查找性能。
  • 支持更高的并发级别,因为锁的竞争减少了。
  • 扩容操作更加高效,因为它可以在不影响其他元素的情况下,逐个处理每个桶(bucket)。

本质和区别

本质

  • 两者都是为了提供线程安全的高效哈希表操作。
  • 都使用了哈希表数据结构,并在必要时进行扩容。

区别

  • 锁机制:JDK 1.7 使用分段锁,而 JDK 1.8 使用锁分离和原子操作。
  • 并发性能:JDK 1.8 的实现提供了更高的并发性能,因为它减少了锁的竞争。
  • 数据结构:JDK 1.8 引入了红黑树来优化长链表的性能。
  • 内存占用:JDK 1.8 的实现可能会有更高的内存占用,因为它使用了更多的锁和更复杂的数据结构。
  • 性能调优:JDK 1.8 提供了更多的构造函数参数来调优性能,例如初始容量和负载因子。

总的来说,JDK 1.8 中的 ConcurrentHashMap 在设计上更加现代化,提供了更好的性能和更高的并发性。这使得它成为处理高并发场景的首选数据结构之一。

源代码解读

ConcurrentHashMap继承自AbstractMap类,实现了ConcurrentMap接口。它主要由SegmentHashEntry两个类组成。

如下是部分源码截图:

在这里插入图片描述

Segment

SegmentConcurrentHashMap的核心。Segment是一个锁,能保证多线程环境下的安全。每个Segment默认关联着一个数组,也就是说,ConcurrentHashMap的数据结构是由多个Segment及其数组组成的。

Segment通过继承ReentrantLock来实现锁的机制。在读取操作的时候,可以允许多个线程同时访问同一个Segment,提高并发性能。而在写入操作时,只能保证同一个Segment中的操作是线程安全的,并不保证多个Segment之间的并发性。

Segment内部还定义了一个内部类HashEntry。因为ConcurrentHashMap是基于哈希表实现的,因此需要一个哈希表的数据结构来存储键值对。HashEntry就是HashMap的一个节点,用于存储键值对。

HashEntry类

HashEntryConcurrentHashMap中的键值对节点。它的定义如下:

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;

    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    final boolean casValue(V cmp, V val) {
        return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
    }

    final boolean casNext(HashEntry<K,V> cmp, HashEntry<K,V> val) {
        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }
    
    // 其他方法...
}

从上面的源码可以看出,HashEntry包含一个哈希值、键、值、下一个节点等信息。其中,volatile关键字修饰的value和next可以保证多线程环境下的可见性和有序性,从而保证线程安全。另外,HashEntry中还定义了两个方法casValue和casNext,用于实现无锁操作,提高并发性能。

拓展:

这是一个 HashEntry 类,作为 ConcurrentHashMap 中的元素存储。

它包含的属性有:

  • hash:键的哈希值,计算方式是通过 key 的 hashCode() 方法得到。
  • key:键,可以为 null。
  • value:值,可以为 null。
  • next:下一个元素的引用,用于解决哈希冲突时使用。

其中,value 和 next 都是 volatile 的,保证多线程环境下的可见性。

还有两个 cas 方法:

  • casValue:尝试原子地将当前 entry 的 value 字段从 cmp 替换为 val,如果替换成功则返回 true,否则返回 false。
  • casNext:尝试原子地将当前 entry 的 next 字段从 cmp 替换为 val,如果替换成功则返回 true,否则返回 false。

这两个 cas 方法都是使用了 java.util.concurrent.atomic 包下的 Unsafe 类,即使用了底层的机器指令实现的原子操作,保证了在多线程环境下的线程安全性。

put方法

ConcurrentHashMap中插入元素的方法是put方法。其实现过程大致如下:

  1. 根据key的哈希值计算出Segment的下标。
  2. 如果Segment为空,使用CAS操作进行创建。
  3. 如果Segment已经存在,则使用锁进行保证多线程环境下的安全。
  4. 在Segment中查找是否存在相同的key,如果存在,则替换掉旧值;如果不存在,则在Segment的链表中添加一个新节点。

代码实现如下所示:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

拓展:

在put方法中,先根据key的哈希值计算出Segment的下标,然后查找是否已经存在相同key的节点。如果存在,则将该节点的值替换成新值;如果不存在,则创建一个新的节点,并添加到Segment的链表中。

在这里插入图片描述

在创建新节点时,要使用CAS操作进行无锁操作,保证多线程环境下的安全。

该代码片段是Java并发哈希表ConcurrentHashMap中的put方法。它接收一个键值对,尝试将其加入哈希表中。首先,它使用哈希函数计算键的哈希值并确定要使用哪个哈希段。然后,它调用相应的哈希段的put方法。

在哈希段的put方法中,它首先尝试获取锁,如果锁不可用,则调用scanAndLockForPut方法。该方法扫描哈希表中的槽位,查找与键匹配的节点。如果找到,则返回该节点的值;否则,创建一个新的节点并将其放入哈希表中。如果哈希表已经过度填充,则重新调整大小并重新散列所有节点。

最后,put方法返回原来键的值,如果该键不存在,则返回null。

get方法

ConcurrentHashMap中获取元素的方法是get。其实现过程大致如下:

  1. 根据key的哈希值计算出Segment的下标。
  2. 在Segment中查询是否存在相同key的节点。
  3. 如果存在,则返回节点的值;如果不存在,则返回null。

代码实现如下所示:

public V get(Object key) {

    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
             (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE));
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

在get方法中,也是根据key的哈希值计算出Segment的下标,然后在该Segment的哈希链表中查询是否存在相同key的节点,如果存在,则返回该节点的值;否则返回null。

在这里插入图片描述

拓展:

这段代码实现了ConcurrentHashMap中的get()方法,用于获取指定key对应的value。

具体实现是先根据key计算出对应的hash值,并根据hash值计算出对应的Segment(分区)对象。如果该Segment存在并且对应的table(哈希桶数组)也存在,就从table中查找对应的entry(键值对)。如果找到了,则返回其value值,否则返回null。

值得注意的是,该方法在获取Segment和table对象时使用了volatile修饰符,以确保多线程环境下的可见性。同时,也利用了UNSAFE类来优化访问过程,提高效率。

size方法

ConcurrentHashMap中获取元素个数的方法是size。其实现过程大致如下:

  1. 遍历所有的Segment,获取每个Segment中的元素个数。
  2. 对所有Segment中的元素个数求和,得到ConcurrentHashMap中元素的总个数。

代码实现如下所示:

public int size() {
    long count = 0L;
    final Segment<K,V>[] segments = this.segments;
    for (int i = 0; i < segments.length; ++i) {
        Segment<K,V> segment = segments[i];
        if (segment != null)
            count += segment.getCount();
    }
    return count >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)count;
}

在计算元素个数时,使用了volatile修饰的count成员变量,保证了线程安全。

在这里插入图片描述

拓展:

这是一个用于计算ConcurrentHashMap中元素数量的方法。该方法首先将所有Segment的元素数量累加起来,最终返回总数量。

具体实现方式为:遍历所有Segment,若Segment不为null,则获取该Segment中元素的数量并累加到count中。最终返回的是int类型,因此当count的值大于或等于Integer.MAX_VALUE时,返回Integer.MAX_VALUE,否则返回count的值。

需要注意的是,由于ConcurrentHashMap采用分段锁策略来保证并发访问的效率,因此在计算元素数量时需要遍历所有的Segment,这可能会影响计算速度。

应用场景案例

ConcurrentHashMap是一个线程安全的哈希表实现,在多线程并发访问时性能表现优异。因此,它适用于以下场景:

  1. 在高并发的web应用中,可以使用ConcurrentHashMap存储session信息,以确保多个请求同时访问session时的线程安全性。

  2. 在多线程的数据处理场景中,可以使用ConcurrentHashMap存储操作的中间结果,以避免使用锁机制造成的性能瓶颈。

  3. 在多线程的缓存场景中,可以使用ConcurrentHashMap作为缓存容器,以支持高并发的读写操作。

  4. 在分布式系统中,可以使用ConcurrentHashMap作为本地缓存,以减少远程调用的次数,提高系统性能。

优缺点分析

优点

  1. 线程安全:ConcurrentHashMap是一个线程安全的哈希表实现,在多线程并发访问时性能表现优异。

  2. 高并发性:ConcurrentHashMap的内部实现是基于Segment的,每个Segment内部都有一个锁来控制并发访问,从而使得多个线程能够同时读取数据,提高了并发性能。

  3. 高效性:ConcurrentHashMap的性能表现很优秀。在高并发读写场景中,性能比Hashtable和SynchronizedMap要好得多。

  4. 可扩展性:ConcurrentHashMap的内部结构是基于哈希表的,因此,在元素数量增加时,可以通过调整Segment的数量来扩展ConcurrentHashMap的容量,从而保证元素的均衡分布。

缺点

  1. 内存占用:由于ConcurrentHashMap的内部实现是基于Segment的,因此它会占用更多的内存空间。

  2. 不支持空值:ConcurrentHashMap的值不能为空。

  3. 不保证元素的排列顺序:ConcurrentHashMap不保证元素的排列顺序,因此不能用于需要保证元素顺序的场景。

测试用例

测试代码演示

下面是一个简单的 ConcurrentHashMap 测试用例:

/**
 * ConcurrentHashMap示例演示
 *
 * @Author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @Date 2023-11-06 16:53
 */
public class ConcurrentHashMapTest {

    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 添加元素
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        // 打印元素
        System.out.println(map);

        // 获取元素
        Integer one = map.get("one");
        System.out.println("one = " + one);

        // 移除元素
        Integer removed = map.remove("two");
        System.out.println("removed = " + removed);
        System.out.println(map);

        // 替换元素
        Integer replaced = map.replace("one", 100);
        System.out.println("replaced = " + replaced);
        System.out.println(map);

        // 遍历元素
        map.forEach((key, value) -> {
            System.out.println(key + " = " + value);
        });
    }
}

测试结果

根据如上测试用例,本地测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加更多的测试数据或测试方法,进行熟练学习以此加深理解。

输出结果:

    {two=2, one=1, three=3}
    one = 1
    removed = 2
    {one=1, three=3}
    replaced = 1
    {one=100, three=3}
    one = 100
    three = 3

实际执行结果如下:

根据如上代码进行分析

测试代码分析

根据如上测试用例,在此我给大家进行深入详细的解读一下测试代码,以便于更多的同学能够理解并加深印象。   该示例演示了ConcurrentHashMap的基本操作,包括添加元素、获取元素、移除元素、替换元素和遍历元素等。其中,ConcurrentHashMap是线程安全的哈希表实现,相较于HashTable和HashMap,其性能更优。在多线程环境下,ConcurrentHashMap能够提供更好的并发性能和可伸缩性。

小结

ConcurrentHashMap是Java中的一个线程安全的哈希表实现,在高并发读写场景中性能表现优异。它的内部实现是基于Segment的,通过锁机制来保证线程安全性,并且可以通过增加Segment的数量来扩展容量,具有很好的可扩展性。但是,由于它的内部结构是基于哈希表的,因此会占用更多的内存空间。同时,ConcurrentHashMap不支持空值,并且不保证元素的排列顺序,需要根据具体的场景选择使用。

总结

本文详细介绍了Java中的线程安全容器ConcurrentHashMap,包括其源码解析、应用场景案例和优缺点分析。ConcurrentHashMap采用了分段锁和哈希表的结构实现,能够保证线程安全和高并发性能。同时,它也具有可扩展性和高效性。但是,ConcurrentHashMap也存在一些缺点,如占用更多的内存空间,不支持空值等。总之,在适当的场景中,使用ConcurrentHashMap能够提高程序的性能和可靠性。

... ...

好啦,这期的内容就基本接近尾声啦,若你想学习更多,你可以看看专栏的导读篇《「滚雪球学Java」教程导航帖》,本专栏致力打造最硬核 Java 零基础系列学习内容,🚀打造全网精品硬核专栏,带你直线超车;欢迎大家订阅持续学习。功不唐捐,久久为功!

「赠人玫瑰,手留余香」,咱们下期拜拜~~

附录源码

如上涉及所有源码均已上传同步在「Gitee」,提供给同学们一对一参考学习,辅助你更迅速的掌握。

☀️建议/推荐你

无论你是计算机专业的学生,还是对编程感兴趣的跨专业小白,都建议直接入手「滚雪球学Java」专栏;该专栏不仅免费,bug菌还郑重承诺,只要你学习此专栏,均能入门并理解Java SE,以全网最快速掌握Java语言,每章节源码均同步「Gitee」,你真值得拥有;学习就像滚雪球一样,越滚越大,带你指数级提升。

码字不易,如果这篇文章对你有所帮助,帮忙给bugj菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。

📣关于我

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 20w+;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!