当前位置:首页 > 技术 > 正文内容

Java哈希表并发演进:从HashMap到ConcurrentHashMap的线程安全机制

访客 技术 2026年10月7日 1

HashMap 的并发缺陷分析

在多线程环境中直接使用 HashMap 会引发数据丢失、结构损坏甚至死循环。其根本原因在于底层数组的索引计算与节点挂载过程缺乏任何同步控制。当多个线程同时执行插入操作且发生哈希冲突时,后执行的线程会直接覆盖先执行线程写入的键值对。此外,在扩容迁移或链表转红黑树的过程中,并发修改会导致节点引用错乱,破坏数据结构的完整性。

public class UnsafeKeyValueStore<K, V> {
    private transient BucketNode<K, V>[] storage;

    static class BucketNode<K, V> {
        final int hashcode;
        final K key;
        V data;
        BucketNode<K, V> link;
    }

    // 并发写入时缺乏同步屏障,索引冲突将直接引发数据覆盖
    public void insert(K k, V v) {
        int idx = computeIndex(k);
        BucketNode<K, V> target = storage[idx];
        if (target == null) {
            storage[idx] = new BucketNode<>(k.hashCode(), k, v, null);
        } else {
            // 多线程同时执行此分支会导致节点覆盖或链表断裂
            target.data = v;
        }
    }
}

Hashtable 的全局同步策略

Hashtable 采用了一种粗粒度的同步方案:直接在所有核心公开方法(如 get、put、remove)上声明 synchronized 关键字。该设计虽然严格保证了线程安全,但锁的作用域覆盖了整个哈希表实例。即使多个线程操作的是完全不同的哈希槽位,也会因为竞争同一把对象监视器锁而被迫串行执行,在高并发场景下会成为严重的性能瓶颈。

public class SynchronizedTable<K, V> {
    private transient TableEntry<K, V>[] entries;

    static class TableEntry<K, V> {
        final int hashcode;
        final K key;
        V data;
        TableEntry<K, V> link;
    }

    // 方法级同步,锁住整个实例,无关槽位的操作也会相互阻塞
    public synchronized V fetch(Object k) {
        // 遍历查找逻辑...
        return null;
    }

    public synchronized V store(K k, V v) {
        // 写入与冲突处理逻辑...
        return null;
    }
}

ConcurrentHashMap JDK 7 的分段锁架构

为突破全局锁的吞吐量限制,JDK 7 引入了分段锁(Segment)设计。底层由 Segment 数组与 HashEntry 数组构成,每个 Segment 继承自 ReentrantLock

在统计元素总数(size())时,采用乐观锁策略:先无锁累加各分段的计数值与修改版本号。若连续两次快照的修改版本号一致,则认为期间无并发写入,直接返回累加结果;若检测到版本变更,则重试一定次数后转为对全部分段加锁的悲观策略,完成精确统计。

public class SegmentedConcurrentMap<K, V> {
    final LockSegment<K, V>[] partitions;

    static class DataItem<K, V> {
        final int hashcode;
        final K key;
        volatile V data;
        volatile DataItem<K, V> link;
    }

    static class LockSegment<K, V> extends ReentrantLock {
        transient volatile DataItem<K, V>[] bucket;

        final V storeItem(K k, int h, V v) {
            DataItem<K, V> node = tryLock() ? null : spinAcquireLock(k, h, v);
            try {
                // 执行分段内安全写入
                return null;
            } finally {
                unlock();
            }
        }

        private DataItem<K, V> spinAcquireLock(K k, int h, V v) {
            while (!tryLock()) {
                // 自旋等待锁释放,避免立即阻塞
            }
            return null;
        }
    }

    public V put(K k, V v) {
        if (v == null) throw new NullPointerException();
        // 定位目标分段并委托给 storeItem 处理
        return null;
    }
}

ConcurrentHashMap JDK 8 的细粒度锁与分散计数

JDK 8 彻底废弃了 Segment 架构,回归到与 HashMap 相似的 Node 数组结构,但同步机制更为精细。插入或更新数据时,仅对目标桶(Bucket)的头节点使用 synchronized 加锁。由于链表遍历与红黑树操作均始于头节点,锁定头节点即可安全控制该槽位的所有变更,临界区大幅缩小,锁竞争概率显著降低。

元素计数方面,摒弃了全局累加模式,转而采用 baseCount 结合 CounterCell 数组的分散计数方案。无竞争时直接通过 CAS 更新基础计数器;发生线程竞争时,将增量分散写入 CounterCell

public class ModernConcurrentMap<K, V> {
    transient volatile BucketNode<K, V>[] table;
    private transient volatile long baseCounter;
    private transient volatile CountCell[] countCells;

    static class BucketNode<K, V> {
        final int hashcode;
        final K key;
        volatile V val;
        volatile BucketNode<K, V> next;
    }

    final V insertValue(K k, V v) {
        if (k == null || v == null) throw new NullPointerException();
        for (BucketNode<K, V>[] tab = table;;) {
            BucketNode<K, V> head = tabAt(tab, index);
            if (head == null) {
                // CAS 尝试初始化空桶
            } else {
                // 仅锁定当前桶的头节点,极大缩小同步范围
                synchronized (head) {
                    // 执行链表尾插或红黑树平衡操作
                }
            }
            updateCount(1L);
            return null;
        }
    }

    private void updateCount(long delta) {
        // 优先 CAS 更新 baseCounter
        // 竞争激烈时将增量分散写入 countCells 数组单元
    }
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。