深入探索Java并发容器:ConcurrentHashMap详解与实战

本文详细介绍了JavaConcurrentHashMap在并发编程中的重要性,包括分段锁机制、链地址法与CAS操作、红黑树转换、JDK1.8后的改进以及线程安全的迭代器。通过实例演示了其实现原理和在实际开发中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

在Java并发编程中,数据结构的线程安全性是至关重要的。当涉及到多线程环境下的键值对存储时,`java.util.concurrent.ConcurrentHashMap`(简称CHM)扮演了无可替代的角色。它巧妙地结合了HashMap的高效性和Hashtable的线程安全性,从而成为高并发场景下首选的数据结构之一。本文将深入剖析ConcurrentHashMap的设计原理、关键特性,并通过实例演示其在实际开发中的应用。

一、分段锁(Segment Locking)

在Java早期版本(JDK 1.7及之前)中,java.util.concurrent.ConcurrentHashMap使用了分段锁机制来实现高效的并发访问。其核心思想是将整个哈希表划分为多个独立的段(Segments),每个段都可以独立地加锁和解锁,从而实现了更细粒度的并发控制。
以下是一个简化的示例,帮助理解分段锁的基本概念:

// 注意:以下代码仅用于演示分段锁的思想,并非真实的ConcurrentHashMap源码

import java.util.concurrent.locks.ReentrantLock;

public class SegmentLockExample {
    // 假设Segment类代表ConcurrentHashMap中的一个分段
    static class Segment<K, V> {
        // 每个Segment内部包含一个类似HashMap的数据结构
        HashEntry<K, V>[] table;
        
        // 分段锁
        final ReentrantLock lock = new ReentrantLock();

        // 插入操作,需要先获取segment对应的锁
        void put(K key, V value) {
            lock.lock();
            try {
                // 省略具体插入逻辑...
                int hash = key.hashCode();
                // 找到对应位置并插入数据
                // ...
            } finally {
                lock.unlock();
            }
        }

        // 其他操作如get、remove等同样需要通过lock保护
    }

    // Segment数组模拟ConcurrentHashMap中的所有分段
    static class ConcurrentHashMap<K, V> {
        final Segment<K, V>[] segments;

        ConcurrentHashMap(int concurrencyLevel, int initialCapacity) {
            // 初始化segments数组大小,根据并发级别计算
            segments = new Segment[concurrencyLevel];
            // 省略其他初始化逻辑...
        }

        // 对外提供的put方法,需要定位到对应的Segment进行操作
        public V put(K key, V value) {
            int hash = key.hashCode();
            int segmentIndex = hash >>> segmentShift; // 计算key所属的Segment索引
            Segment<K, V> segment = segments[segmentIndex];
            segment.put(key, value);
            return null; // 返回值仅为示意
        }
    }

    // 模拟Segment内部存储节点的HashEntry类
    static class HashEntry<K, V> {
        K key;
        V value;
        HashEntry<K, V> next;

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



二、链地址法结合CAS(Compare and Swap)

在Java 8及更高版本的java.util.concurrent.ConcurrentHashMap中,虽然摒弃了分段锁(Segment Locking)的设计,但仍然使用链地址法来解决哈希冲突,并结合CAS(Compare and Swap)操作实现无锁化的并发控制。以下是一个简化的示例以说明其基本原理:

// 注意:以下代码仅用于演示ConcurrentHashMap中的CAS和链地址法的思想,并非真实的源码

import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicReferenceArray;

public class ConcurrentHashMapCasExample {
    static final Unsafe unsafe;
    // 假设Node类是ConcurrentHashMap内部的一个节点结构
    static class Node<K, V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K, V> next;

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

    // 使用AtomicReferenceArray模拟ConcurrentHashMap的数组部分
    static final class Table<K, V> extends AtomicReferenceArray<Node<K, V>> {
        Table(int length) {
            super(length);
        }
    }

    // 初始化Unsafe实例,用于CAS操作
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private final Table table; // 实际的ConcurrentHashMap会动态调整table大小

    ConcurrentHashMap() {
        table = new Table<>(initialCapacity); // 初始化表的大小
    }

    // 使用CAS进行put操作(简化版)
    public V put(K key, V value) {
        int hash = spread(key.hashCode());
        int index = hash & (table.length() - 1); // 计算桶索引

        for (Node<K, V> e = table.get(index); e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                // 如果找到了键值对,则使用CAS更新value
                while (!unsafe.compareAndSwapObject(e, Node.valOffset, e.val, value)) {
                    // 当前值被其他线程修改,循环重试直到成功
                }
                return e.val; // 返回旧值
            }
        }

        // 添加新节点到链表头部
        Node<K, V> newNode = new Node<>(hash, key, value, table.get(index));
        while (!table.compareAndSet(index, null, newNode)) { // 空桶直接添加
            Node<K, V> oldNode = table.get(index);
            newNode.next = oldNode; // 已有节点则插入链表头部
            if (table.compareAndSet(index, oldNode, newNode)) {
                return null; // 成功插入返回null
            }
        }
        return null; // 插入新节点返回null
    }

    // 静态方法获取对象field偏移量,实际ConcurrentHashMap内部会有类似的方法
    private static long valOffset;

    static {
        try {
            valOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("val"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }

    // 扩散函数,简化版,实际ConcurrentHashMap内部有更复杂的处理
    private static int spread(int h) {
        return (h ^ (h >>> 16)) & Integer.MAX_VALUE;
    }
}

上述示例展示了如何利用CAS操作在链表上安全地更新或插入元素。实际上,在真正的ConcurrentHashMap中,为了进一步提高性能,还引入了红黑树等数据结构,当链表长度达到阈值时会转化为红黑树以优化查询效率。同时,扩容、迁移等操作也都是通过CAS原子操作保证线程安全的。 



三、红黑树转换

 在Java 8及更高版本的java.util.concurrent.ConcurrentHashMap中,当链表长度超过一定阈值时,会将链表转换为红黑树以优化查询性能。这个过程是由内部类TreeNode(继承自Node)和相关方法实现的。以下是一个简化的示例来说明红黑树转换的基本原理:

// 注意:以下代码是基于ConcurrentHashMap实际逻辑简化模拟

import java.util.concurrent.atomic.AtomicReferenceArray;

public class ConcurrentHashMapRBTreeExample {

    // 假设Node是基础节点类,而TreeNode是用于构建红黑树的节点类
    static class Node<K, V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K, V> next; // 链表结构

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

    static class TreeNode<K, V> extends Node<K, V> {
        TreeNode<K, V> parent; // 红黑树中的父节点引用
        TreeNode<K, V> left, right; // 左右子树引用
        boolean red; // 是否是红色节点(红黑树特性)

        TreeNode(int hash, K key, V val, Node<K, V> next) {
            super(hash, key, val, null);
            this.left = this.right = null;
            this.red = true; // 新创建的节点默认为红色
        }

        // 省略了红黑树相关的旋转、着色等平衡操作...
    }

    private final AtomicReferenceArray<Node> table; // 实际ConcurrentHashMap使用此数组存储数据
    private static final int TREEIFY_THRESHOLD = 8; // 转换为红黑树的阈值

    public V put(K key, V value) {
        // ...省略定位到桶的操作...

        // 当链表过长时,进行树化
        if (e instanceof TreeNode) { // 如果已经是树节点,则直接插入
            TreeNode<K, V> p = (TreeNode<K, V>) e;
            TreeNode<K, V> newNode = new TreeNode<>(hash, key, value, null);
            TreeNode<K, V> r = tryInsertTreeNode(root, p, newNode); // 插入新节点并保持红黑树性质
            return r == null ? null : r.val;
        } else if (count >= TREEIFY_THRESHOLD) { // 链表长度达到阈值,尝试树化
            TreeNode<K, V> hd = treeifyBin(tab, i); // 将链表转换为红黑树
            setTabAt(tab, i, hd); // 更新桶的头节点为红黑树根节点
        }

        // ...省略后续更新节点value和其他并发控制逻辑...
    }

    // 尝试将一个链表bin转换成红黑树
    private TreeNode<K, V> treeifyBin(Node<K, V>[] tab, int index) {
        Node<K, V> b; // 指向当前桶的第一个元素
        if ((tab != null) && (tab.length > MIN_TREEIFY_CAPACITY)
                && (b = tabAt(tab, index)) != null
                && b.hash != MOVED) { // 判断是否满足树化条件
            // 省略了实际树化的过程,包括将链表转换为红黑树的具体算法...
            return new TreeNode<>(...); // 返回新构建的红黑树根节点
        } else {
            // 不满足树化条件则进行扩容或其他处理
            // ...
        }
    }

    // 简化版的红黑树插入方法,实际ConcurrentHashMap内部有更复杂的平衡调整操作
    private TreeNode<K, V> tryInsertTreeNode(TreeNode<K, V> root, TreeNode<K, V> parent, TreeNode<K, V> newNode) {
        // 省略了插入新节点后的红黑树旋转和重新着色等平衡操作...
        return newNode; // 示例中仅返回新插入的节点
    }
}

实际的ConcurrentHashMap中,红黑树的插入、删除、查找以及旋转、着色等操作都实现了严格遵循红黑树特性的复杂逻辑,这里为了简化起见,并没有展示这些细节。但上述代码足以表达出ConcurrentHashMap中链表转红黑树的大致流程 



四、JDK 1.8后的改进

在Java 1.8及更高版本中,java.util.concurrent.ConcurrentHashMap进行了显著的优化和改进。以下主要从数据结构、并发控制以及操作方法等方面进行讲解,并提供简化版的代码示例。


数据结构改进

  • 摒弃Segment(分段锁)设计:在JDK 1.7之前,ConcurrentHashMap使用了Segment数组配合HashEntry链表实现分段锁,而在JDK 1.8中,放弃了这种设计,改为直接使用Node对象数组+链表/红黑树的数据结构,并且通过CAS和volatile关键字保证并发访问的安全性。
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    volatile V val; // 使用volatile保证可见性
    volatile Node<K, V> next;

    // ...省略构造函数和其他内部方法...
}

transient volatile Node<K,V>[] table;
  • 引入红黑树转换:当链表长度超过8时,会将链表转化为红黑树以提高查找效率。同时,当红黑树节点数量低于6时,又会将其退化为链表。

 并发控制改进

  • 无锁化:利用CAS(Compare And Swap)操作来避免使用传统的互斥锁,从而提升并发性能。
  • 扩容策略:在扩容过程中采用了一种更加高效的并发迁移算法,多个线程可以并行地帮助完成扩容任务,极大地减少了因扩容导致的阻塞时间。

操作方法改进

  • 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 (binCount != 0) {
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i); // 转换为红黑树
                    }
                }
            }
        }
    }
}



五、扩容机制

Java 1.8版本的java.util.concurrent.ConcurrentHashMap扩容机制在保证线程安全的前提下,采用了更为高效的并发扩容方式。当容器中的元素数量超过负载因子所设定的阈值时,

ConcurrentHashMap会自动进行扩容。扩容的具体步骤包括:


1.初始化与扩容触发条件:初始容量通过构造函数设置,如果未指定,则使用默认值,并且容量始终是2的幂次方。当实际存储的节点数(包括链表和树节点)达到容量乘以负载因子的大小时,将触发扩容。 

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // Find a power of 2 >= initialCapacity
    int capacity = roundUpToPowerOf2(initialCapacity);
    this.loadFactor = loadFactor;
    table = new Node[capacity];
    threshold = (int)(capacity * loadFactor); // 设置阈值
}

// 辅助方法用于向上取整到最近的2的幂
static final int roundUpToPowerOf2(int number) {
    return Integer.highestOneBit((number - 1) << 1);
}

2.扩容过程:扩容过程中,新的容量通常为旧容量的两倍。扩容不是一次性完成,而是采用了一种并行迁移的方式。当一个线程发现需要扩容时,它首先标记当前桶(Node数组的一个位置)的状态为MOVED,然后创建一个新的、更大的数组,并开始迁移原数组中的元素到新数组中。其他线程在执行put操作时,如果遇到MOVED状态的桶也会参与到扩容迁移的过程中。
以下是简化版的扩容逻辑示例: 

// 假设transfer方法是处理扩容的核心逻辑
final Node<K,V>[] transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // ...省略部分代码...
    while (s-- > 0) {
        Node<K,V> f; int n, i, fh;
        if ((f = tabAt(tab, i)) != null && (fh = f.hash) >= 0) {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= UNTREEIFY_THRESHOLD) { // 树节点转链表
                        TreeNode<K,V> ft = (TreeNode<K,V>)f;
                        TreeNode<K,V> t = splitTreeNode(ft, nextTab, i, false);
                        ln = t.lo;
                        hn = t.hi;
                    } else {
                        // 链表节点直接拆分
                        ArrayList<Node<K,V>> al = new ArrayList<>(2);
                        al.add(f);
                        for (Node<K,V> e = f.next; e != null; e = e.next)
                            al.add(e);
                        int alSize = al.size();
                        ln = null;
                        hn = null;
                        for (int j = 0; j < alSize; j++) {
                            Node<K,V> e = al.get(j);
                            int h = e.hash;
                            int k = h & (nextTab.length - 1);
                            if ((ln == null) || (h < 0))
                                ln = new Node<K,V>(h, e.key, e.val, null);
                            else
                                setTabAt(nextTab, k, e);
                        }
                    }
                    setTabAt(nextTab, i, ln);
                    setTabAt(tab, i, fwd); // 将老桶置为转发节点
                    // 同理处理高位节点hn
                }
            }
        }
    }
    return nextTab;
}

// 调用扩容的方法
void resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    int newCap = oldCap << 1;
    Node<K,V>[] newTab = new Node[newCap];
    //...省略转移数据的部分...
    table = newTab;
    threshold = (int)(newCap * loadFactor);
}



六、线程安全的迭代器

Java的ConcurrentHashMap提供了线程安全的迭代器Iterator,可以在多线程环境下安全地遍历ConcurrentHashMap中的元素。与HashMap不同,ConcurrentHashMap的迭代器不会抛出ConcurrentModificationException异常,即使在迭代过程中有其他线程修改了ConcurrentHashMap的内容。


下面是一个使用ConcurrentHashMap的线程安全迭代器的示例:

import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        // 创建一个ConcurrentHashMap
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        
        // 添加一些元素
        map.put("a", 1);
        map.put("b", 2);
        map.put("c", 3);
        
        // 获取一个迭代器
        Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
        
        // 遍历ConcurrentHashMap
        while(iterator.hasNext()) {
            Map.Entry<String, Integer> entry = iterator.next();
            String key = entry.getKey();
            Integer value = entry.getValue();
            
            System.out.println(key + " = " + value);
        }
    }
}

在上面的示例中,我们首先创建了一个ConcurrentHashMap对象,并向其中添加了一些元素。然后,我们通过调用entrySet()方法获取了ConcurrentHashMap中的所有键值对的集合,并通过调用iterator()方法获取了一个迭代器对象。最后,我们使用该迭代器对象遍历了ConcurrentHashMap中的所有元素。


需要注意的是,虽然ConcurrentHashMap的迭代器是线程安全的,但是它只能保证在遍历过程中不会出现ConcurrentModificationException异常,而不能保证迭代器返回的元素不会被其他线程修改或删除。因此,在多线程环境下,如果需要保证迭代器返回的元素不被修改或删除,需要使用额外的同步机制来保护ConcurrentHashMap中的元素。

总结

ConcurrentHashMap以其独特的设计理念和精巧的实现方式,成功解决了多线程环境下的数据共享问题。了解并掌握ConcurrentHashMap的工作原理及其在并发编程中的最佳实践,对于提高Java应用程序的性能和稳定性具有重要意义。开发者应当根据实际需求选择合适的API方法,充分利用ConcurrentHashMap所提供的并发优势。 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小码快撩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值