ConcurrentHashMap 实现原理:深度解析

目录

ConcurrentHashMap 实现原理:深度解析

一、引言

二、ConcurrentHashMap 的存储结构(JDK1.8)

三、ConcurrentHashMap 的并发安全实现

四、ConcurrentHashMap 的性能优化

(一)锁粒度优化

(二)引入红黑树

(三)并发扩容机制

(四)元素个数统计优化


一、引言

在面试中,ConcurrentHashMap 的实现原理是一个常考的知识点。不少小伙伴都对这个问题感到困惑,今天我们就来详细剖析一下,帮助大家在面试中脱颖而出。同时,我还为大家准备了福利,在评论区置顶处可以免费领取 30 万字的面试文档,涵盖各大一线大厂面试真题及详细解答哦。

二、ConcurrentHashMap 的存储结构(JDK1.8)

ConcurrentHashMap 是由数组、单向链表和红黑树组成的。当我们初始化一个 ConcurrentHashMap 实例时,默认会初始化一个长度为 16 的数组。由于它依然是一个哈希表,所以必然存在哈希冲突问题。它采用链式寻址法来解决哈希冲突,示例如下(这里是简单的 Java 代码模拟哈希冲突的链式存储,与 ConcurrentHashMap 内部原理类似):

import java.util.LinkedList;

class HashEntry {
    int key;
    String value;

    public HashEntry(int key, String value) {
        this.key = key;
        this.value = value;
    }
}

class MyHashMap {
    private LinkedList[] table;
    private int size;

    public MyHashMap(int capacity) {
        table = new LinkedList[capacity];
        for (int i = 0; i < capacity; i++) {
            table[i] = new LinkedList<>();
        }
    }

    public void put(int key, String value) {
        int index = key % table.length;
        for (HashEntry entry : table[index]) {
            if (entry.key == key) {
                entry.value = value;
                return;
            }
        }
        table[index].add(new HashEntry(key, value));
    }
}

当哈希冲突较多时,链表可能会过长,这种情况下,数据元素的查询复杂度会变成 O (n)。因此在 JDK1.8 中引入了红黑树机制,当数组长度大于 64 并且链表长度大于等于 8 时,单向链表就会转化为红黑树。另外,随着 ConcurrentHashMap 的动态扩容,一旦链表长度小于 6,红黑树就会退化成单向链表。

三、ConcurrentHashMap 的并发安全实现

ConcurrentHashMap 本质上是一个哈希表,功能和普通哈希表类似,但它在哈希表的基础上提供了并发安全的实现。其并发安全主要是通过对指定的 node 节点加锁来保证数据更新的安全性。这种在并发性能和数据安全性之间平衡的设计,在很多地方都有类似情况,比如 CPU 的三级缓存、MySQL 的 buffer pool、synchronized 的锁升级等。

四、ConcurrentHashMap 的性能优化

(一)锁粒度优化

在 JDK1.8 中,ConcurrentHashMap 锁的粒度是数组的某一个节点,而在 JDK1.7 中,锁定的是一个 segment,锁的范围更大,因此 JDK1.7 性能更低。

(二)引入红黑树

引入红黑树降低了数据查询的时间复杂度,红黑树的时间复杂度是 O (log n)。

(三)并发扩容机制

当数组长度不够时,ConcurrentHashMap 需要对数组进行扩容。在扩容方面,它引入了多线程并发扩容的机制。简单来说,就是多个线程对原始数据进行分片以后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中的数据迁移效率。以下是一个简单的多线程处理数据的 Java 示例(与 ConcurrentHashMap 并发扩容思想类似):

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DataMigrationExample {
    public static void main(String[] args) {
        List dataList = new ArrayList<>();
        // 模拟填充数据
        for (int i = 0; i < 100; i++) {
            dataList.add(i);
        }
        // 创建一个线程池,这里使用固定大小为4的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        int sliceSize = dataList.size() / 4;
        for (int i = 0; i < 4; i++) {
            int startIndex = i * sliceSize;
            int endIndex = (i == 3)? dataList.size() : (i + 1) * sliceSize;
            List sliceData = dataList.subList(startIndex, endIndex);
            executorService.execute(() -> {
                // 这里模拟每个线程对数据分片的处理,比如迁移操作
                for (Integer data : sliceData) {
                    System.out.println(Thread.currentThread().getName() + "处理数据: " + data);
                }
            });
        }
        executorService.shutdown();
    }
}

(四)元素个数统计优化

ConcurrentHashMap 中有一个 size 方法来获取总的元素个数。在多线程并发场景中,要保证准确性地实现元素个数累加,性能是非常低的。它在这方面的优化主要体现在两个点上:

  1. 当线程竞争不激烈的时候,直接采用 CAS(比较并交换)的机制来实现元素个数的原子递增。
  2. 如果线程竞争激烈的情况下,使用一个数组来维护元素个数,如果要增加总元素个数,则直接从数组中随机选择一个,再通过 CAS 来实现原子递增。其核心思想是引入了一个数组来实现对并发更新的负载均衡,降低了锁的竞争。

希望通过这篇文章,大家对 ConcurrentHashMap 的实现原理有更深入的理解,无论是应对面试还是在实际开发中都能有所帮助。咱们下期再见!

你可能感兴趣的:(java,java)