HashMap 和 ConcurrentHashMap 的区别

1. 线程安全性 (核心区别):

  • HashMap (非线程安全):

    • 问题: 在多线程环境下,如果多个线程同时对 HashMap 进行修改操作(例如 put()remove()clear() 等),会导致数据不一致,甚至可能导致 HashMap 的内部结构损坏,例如出现链表循环,最终导致死循环。

    • 原因: HashMap 的内部操作(例如插入元素、删除元素、扩容等)都没有进行任何线程同步。

  • ConcurrentHashMap (线程安全):

    • 保证: ConcurrentHashMap 通过复杂的锁机制保证了并发访问的安全性,多个线程可以同时对 ConcurrentHashMap 进行读写操作,而不会导致数据不一致。

2. 数据结构 & 锁机制 (底层实现):

FeatureHashMapConcurrentHashMap (JDK 1.7)ConcurrentHashMap (JDK 1.8+)
数据结构数组 + 链表/红黑树数组 + 链表/红黑树数组 + 链表/红黑树
锁机制分段锁 (Segment Lock)CAS + synchronized
锁粒度N/A段级别 (Segment level)节点级别 (Node level)
并发写操作不同步,可能导致数据不一致同一 Segment 内写操作互斥,不同 Segment 可并行不同 Node 可并行,同一 Node 内写操作互斥
null 键/值允许一个 null 键, 多个 null不允许 null 键和 null不允许 null 键和 null
迭代器Fail-fast (快速失败)Fail-safe (安全失败), 弱一致性Fail-safe (安全失败), 弱一致性
扩容机制单线程执行分段独立扩容多线程并发扩容
size()/isEmpty()精确值不一定是精确值不一定是精确值
  • HashMap: HashMap 的数据结构是数组 + 链表/红黑树。
  • ConcurrentHashMap (JDK 1.7 及之前):
    • 分段锁 (Segment Lock): ConcurrentHashMap 内部维护了一个 Segment 数组,每个 Segment 都是一个小的哈希表,并且拥有自己的锁。
    • 锁粒度: 锁的粒度是 Segment 级别。 当一个线程访问一个 Segment 时,它会获取该 Segment 的锁,其他线程就不能访问该 Segment,但可以访问其他 Segment
    • 写操作: 写操作需要获取对应 Segment 的锁,因此同一 Segment 内的写操作是互斥的,但不同 Segment 之间的写操作可以并行进行。
    • 读操作: 读操作通常不需要加锁(除非正在进行扩容),因此读操作的并发性很高。
    • 扩容: 每个 Segment 独立扩容。
  • ConcurrentHashMap (JDK 1.8 及之后):
    • CAS + synchronized: JDK 1.8 及之后,ConcurrentHashMap 放弃了分段锁,而是采用 CAS (Compare and Swap) + synchronized 的方式来保证并发安全。
    • 锁粒度: 锁的粒度降低到节点级别 (Node level)。
    • Node 状态: 每个哈希桶的头节点 (Node) 有几种状态:
      • null: 表示该哈希桶为空。
      • FWD: 表示该哈希桶正在进行扩容。
      • RESERVED: 表示该哈希桶已经被一个线程锁定。
      • 其他值: 表示该哈希桶的头节点的哈希值。
    • 写操作:
      1. 使用 CAS 操作尝试将新节点添加到哈希桶的头部。
      2. 如果 CAS 操作失败(说明有其他线程正在修改该哈希桶),则使用 synchronized 关键字对该哈希桶的头节点进行加锁。
      3. 加锁成功后,再次尝试将新节点添加到哈希桶。
    • 读操作: 读操作通常不需要加锁(除非正在进行扩容或遇到 FWD 节点)。
    • 扩容: 支持多线程并发扩容,提高扩容效率。

3. 并发性能:

  • HashMap 在单线程环境下,HashMap 的性能非常高。 但在多线程环境下,由于没有锁机制,性能会急剧下降,甚至出现错误。
  • ConcurrentHashMap ConcurrentHashMap 在多线程环境下具有非常高的并发性能。
    • JDK 1.7: 分段锁机制允许多个线程同时访问不同的段,提高了并发性能。
    • JDK 1.8: CAS + synchronized 进一步降低了锁的粒度,减少了锁竞争,提高了并发性能。

4. null 键和 null 值:

  • HashMap 允许一个 null 键和多个 null 值。 null 键的哈希值被视为 0。
  • ConcurrentHashMap 不允许 null 键和 null 值。
    • 原因:
      • 并发歧义: 在并发环境下,如果允许 null 值,get(key) 方法返回 null 时,无法区分是 key 不存在,还是 key 对应的 value 就是 null。 这会导致程序逻辑错误。
      • 简化实现: 不允许 null 值可以简化 ConcurrentHashMap 的内部实现,提高性能。

5. 迭代器:

  • HashMap Fail-fast (快速失败)

    • 机制: 在迭代过程中,如果其他线程修改了 HashMap 的结构(例如添加或删除元素),迭代器会立即抛出 ConcurrentModificationException 异常。
    • 目的: 尽早发现并发修改,避免程序继续运行,从而导致更严重的问题。
  • ConcurrentHashMap Fail-safe (安全失败), 弱一致性

    • 机制: 在迭代过程中,如果其他线程修改了 ConcurrentHashMap 的结构,迭代器不会抛出异常,而是继续遍历。 但迭代器不能保证遍历的结果是最新的。
    • 原因: ConcurrentHashMap 的迭代器在创建时,会创建一个快照 (snapshot),迭代器遍历的是这个快照。
    • 弱一致性: 迭代器只能反映 ConcurrentHashMap 在创建迭代器时的状态,不能反映后续的修改。

6. size() 和 isEmpty() 方法:

  • HashMap size()isEmpty() 方法返回的是精确的值。
  • ConcurrentHashMap size()isEmpty() 方法返回的不一定是精确的值。
    • 原因: 在并发环境下,ConcurrentHashMap 的大小可能随时发生变化。 为了避免获取全局锁,size()isEmpty() 方法通常不会获取所有段的锁,而是对各个段的大小进行累加。 因此,返回的值可能不是精确的。
    • JDK 1.8改进: JDK1.8 中引入了 mappingCount 方法, 返回当前 map 的大小, 如果超出了 Integer.MAX_VALUE 则返回 Integer.MAX_VALUE

7. 应用场景总结:

场景推荐使用理由
单线程环境HashMap性能更高,没有锁开销。
多线程环境,不需要并发安全HashMap (但需要外部同步)如果可以确定多线程不会同时修改 HashMap,可以使用外部同步机制(例如 Collections.synchronizedMap)。
多线程环境,需要并发安全ConcurrentHashMap提供线程安全的并发访问。
需要存储 null 键或 null 值 (单线程)HashMapConcurrentHashMap 不允许 null 键和 null 值。
迭代过程中不允许修改 (Fail-fast)HashMapConcurrentHashMap 的迭代器是 Fail-safe 的。
size()/isEmpty() 结果要求绝对精确谨慎选择,通常 ConcurrentHashMap 不保证ConcurrentHashMapsize()/isEmpty() 在并发环境下可能不精确。

8. 代码示例:

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class HashMapVsConcurrentHashMap {

    public static void main(String[] args) throws InterruptedException {

        // 使用 HashMap (非线程安全,演示并发问题)
        Map<Integer, String> hashMap = new HashMap<>();
        testConcurrency(hashMap); // 多线程同时修改 HashMap

        // 使用 Collections.synchronizedMap (线程安全)
        Map<Integer, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
        testConcurrency(synchronizedMap); // 但性能较低

        // 使用 ConcurrentHashMap (线程安全,性能较高)
        ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();
        testConcurrency(concurrentHashMap);
    }

    static void testConcurrency(Map<Integer, String> map) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            final int threadId = i;
            executor.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    map.put(threadId * 1000 + j, "Value" + (threadId * 1000 + j));
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);

        System.out.println(map.getClass().getSimpleName() + " size: " + map.size()); // HashMap 的大小可能不正确

        // 尝试迭代 HashMap (可能抛出 ConcurrentModificationException)
        // for (Map.Entry<Integer, String> entry : map.entrySet()) {
        //     System.out.println(entry.getKey() + ": " + entry.getValue());
        // }
    }
}

总结:

HashMapConcurrentHashMap 是 Java 中两种常用的哈希表实现。 HashMap 适用于单线程环境,性能较高,但不是线程安全的。 ConcurrentHashMap 适用于多线程环境,提供线程安全的并发访问,但牺牲了一定的性能。 在选择使用哪种哈希表时,需要根据具体的应用场景和需求进行权衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰糖心书房

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

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

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

打赏作者

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

抵扣说明:

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

余额充值