java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > HashMap和ConcurrentHashMap的区别

Java中的HashMap和ConcurrentHashMap区别和适用场景

作者:繁川落雨

HashMap和ConcurrentHashMap在对null值的处理、线程安全性、性能等方面存在显著的区别,HashMap允许键和值为null,适用于单线程环境下的数据存储和查询场景;而ConcurrentHashMap不允许键和值为null,适用多线程环境下的数据存储和查询场景,具有线程安全性和较高的并发性能

HashMap和ConcurrentHashMap在对null值的处理、线程安全性、性能等方面存在显著的区别。HashMap允许键和值为null,适用于单线程环境下的数据存储和查询场景,具有较高的性能和简单的使用方式;而ConcurrentHashMap不允许键和值为null,适用于多线程环境下的数据存储和查询场景,具有线程安全性和较高的并发性能。 

1. 对null值的处理

1.1 HashMap对null值的处理

HashMap允许键(key)和值(value)都为null。这种设计使得HashMap在某些场景下更加灵活。例如,在处理一些可能存在空值的数据源时,可以直接将数据存储到HashMap中,而不需要额外的非空判断或转换。以下是一个简单的示例代码:

HashMap<String, Object> map = new HashMap<>();
map.put(null, null); // 正常执行,key 和 value 都为 null
if (map.containsKey(null)) {
    System.out.println("存在 null");
} else {
    System.out.println("不存在 null");
}

执行上述代码,控制台会输出“存在 null”,表明HashMap成功地将null作为键和值存储,并且可以通过containsKey(null)方法准确地判断出null键的存在。

1.2 ConcurrentHashMap对null值的处理

与HashMap不同,ConcurrentHashMap不允许键(key)和值(value)为null。如果尝试将null作为键或值插入到ConcurrentHashMap中,会抛出NullPointerException异常。以下是两个示例代码:

ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put(null, "javacn.site"); // 抛出 NullPointerException
String key = "www.Javacn.site";
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put(key, null); // 抛出 NullPointerException

在上述两个代码片段中,无论是将null作为键还是值插入到ConcurrentHashMap中,都会导致程序异常终止,并抛出NullPointerException异常。

1.3 为什么ConcurrentHashMap不能插入null?

要理解ConcurrentHashMap为什么不能插入null值,我们需要从其源码层面进行分析。以下是ConcurrentHashMap添加元素时的部分核心源码:

// 添加 key 和 value
public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 如果 key 或 value 为 null 的话直接抛出空指针异常
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 忽略其他代码......
}

从上述源码可以看出,在putVal方法的第一行,ConcurrentHashMap就对key和value进行了null检查,如果发现它们中的任何一个为null,就会直接抛出NullPointerException异常。这种设计是ConcurrentHashMap的一个明确的约束条件,旨在避免因null值引起的潜在问题。

1.4 更深层次的原因

那么,为什么ConcurrentHashMap的实现源码中要明确禁止key或value为null呢?这要从ConcurrentHashMap的使用场景和并发环境下的特殊性来分析。

1.4.1 二义性问题

在并发环境下,如果允许ConcurrentHashMap的key或value为null,就会存在经典的“二义性问题”。二义性问题指的是代码或表达式存在多种理解或解释,导致程序的含义不明确或模糊。对于ConcurrentHashMap来说,null值的二义性主要体现在以下两个方面:

如果ConcurrentHashMap允许插入null值,那么在并发环境下,当我们查询某个键时,得到的null值就无法明确区分是上述哪种情况,从而导致二义性问题。

1.4.2 HashMap的可证伪性

相比之下,HashMap允许插入null值,但它不怕二义性问题的原因在于,HashMap是为单线程环境设计的。在单线程环境下,二义性问题是可被证明真伪的。例如,当我们给HashMap的key设置为null时,可以通过hashMap.containsKey(key)的方法来区分这个null值到底是存入的null,还是压根不存在的null。因为单线程环境下,数据的修改和查询是顺序执行的,不会受到其他线程的干扰,所以二义性问题可以被明确地解决。

1.4.3 ConcurrentHashMap的不可证伪性

而ConcurrentHashMap是为多线程环境设计的,多线程下的二义性问题是不能被证明真伪的。因为在证明二义性问题的同时,可能会有其他线程影响执行结果,导致结果不准确。例如,当ConcurrentHashMap未设置key为null时,可能会出现以下场景:一个线程A调用了concurrentHashMap.containsKey(key),期望返回的结果是false,但在调用该方法之后,未返回结果之前,线程B又调用了concurrentHashMap.put(key, null)存入了null值,那么线程A最终返回的结果就是true了。这个结果与线程A之前预想的false完全不一样,这就是不能被证伪的二义性问题。

为了避免这种复杂的二义性问题,ConcurrentHashMap选择在源码中明确禁止null值作为key或value,从而简化了并发环境下的数据管理逻辑,确保了数据的一致性和准确性。

2. 线程安全性

2.1 HashMap的线程安全性

HashMap是非线程安全的,这意味着在多线程环境下使用HashMap时,可能会遇到各种并发问题,如数据丢失、数据重复、死锁等。具体来说,HashMap的线程不安全性主要体现在以下几个方面:

2.2 ConcurrentHashMap的线程安全性

与HashMap不同,ConcurrentHashMap是线程安全的,它通过多种机制来保证在多线程环境下的安全性。以下是ConcurrentHashMap实现线程安全性的主要机制:

2.3 线程安全性对性能的影响

线程安全性对性能有着直接的影响。对于HashMap来说,由于其非线程安全的特性,在单线程环境下可以提供较高的性能,因为不需要进行额外的锁操作和同步处理。然而,在多线程环境下,HashMap的性能会受到严重影响,因为需要额外的同步机制来保证线程安全,如使用Collections.synchronizedMap方法对HashMap进行包装,或者在使用HashMap时手动进行同步处理。

相比之下,ConcurrentHashMap由于其线程安全的特性,在多线程环境下可以提供较高的性能。它通过分段锁、CAS操作和锁分离等机制,减少了锁的粒度和锁的争用,从而提高了并发性能。在多线程环境下,多个线程可以同时对ConcurrentHashMap进行读写操作,而不会出现严重的性能瓶颈。当然,在单线程环境下,ConcurrentHashMap的性能可能会略低于HashMap,因为其内部的线程安全机制会带来一定的开销。

3. 性能比较

3.1 时间复杂度

从时间复杂度的角度来看,HashMap和ConcurrentHashMap在大多数操作上的时间复杂度都是O(1),即常数时间复杂度。这是因为它们都是基于哈希表实现的,通过计算键的哈希值来快速定位对应的桶(bucket),从而实现快速的插入、删除和查找操作。

然而,在某些情况下,时间复杂度可能会退化到O(n),即线性时间复杂度。例如,当哈希表中出现大量哈希冲突时,即多个键的哈希值相同或相近,导致它们被分配到同一个桶中,此时需要遍历桶中的链表或红黑树来找到对应的键值对,时间复杂度会退化到O(n)。不过,这种情况在正常情况下是较少出现的,因为良好的哈希函数和合理的扩容机制可以有效地减少哈希冲突的发生。

3.2 并发性能

在并发性能方面,ConcurrentHashMap显然优于HashMap。如前所述,ConcurrentHashMap通过分段锁、CAS操作和锁分离等机制,减少了锁的粒度和锁的争用,从而提高了并发性能。在多线程环境下,多个线程可以同时对ConcurrentHashMap进行读写操作,而不会出现严重的性能瓶颈。

相比之下,HashMap在多线程环境下需要额外的同步机制来保证线程安全,这会大大降低其并发性能。例如,使用Collections.synchronizedMap方法对HashMap进行包装时,会对所有的操作进行同步处理,导致多个线程在操作HashMap时需要排队等待,从而出现严重的性能瓶颈。

3.3 内存占用

从内存占用的角度来看,ConcurrentHashMap通常会比HashMap占用更多的内存。这是因为ConcurrentHashMap为了实现线程安全,需要额外的数据结构和锁机制。例如,在早期的版本中,ConcurrentHashMap使用分段锁时,每个段都需要占用一定的内存空间。此外,ConcurrentHashMap在扩容时也需要进行更多的内存分配和数据迁移操作,从而增加了内存的占用。

相比之下,HashMap的内存占用相对较少,因为它不需要额外的锁机制和复杂的数据结构。不过,随着ConcurrentHashMap的不断优化,其内存占用情况也在逐渐改善。例如,在Java 8及以后的版本中,ConcurrentHashMap采用了新的数据结构和优化算法,减少了内存的占用。

4. 使用场景

4.1 HashMap的使用场景

HashMap适用于单线程环境下的数据存储和查询场景。由于其非线程安全的特性,在单线程环境下可以提供较高的性能,且使用起来相对简单。以下是一些典型的使用场景:

4.2 ConcurrentHashMap的使用场景

ConcurrentHashMap适用于多线程环境下的数据存储和查询场景。由于其线程安全的特性,在多线程环境下可以提供较高的性能和数据一致性保证。以下是一些典型的使用场景:

5. 总结

在实际开发中,我们需要根据具体的使用场景和需求来选择合适的Map实现类。如果是在单线程环境下,且需要处理可能存在null值的数据,可以选择使用HashMap;如果是在多线程环境下,需要保证数据的线程安全性和一致性,可以选择使用ConcurrentHashMap。此外,还可以根据性能要求、内存占用等因素来综合考虑,以选择最适合的Map实现类来满足实际需求。

到此这篇关于Java中的HashMap和ConcurrentHashMap区别和适用场景的文章就介绍到这了,更多相关HashMap和ConcurrentHashMap的区别内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文