【Java编程进阶之路 01】深入探索:HashMap、ConcurrentHashMap与HashTable的演进之路

简介: HashMap、ConcurrentHashMap与HashTable均为Java中的哈希表实现。HashMap非线程安全但性能高,适用于单线程;HashTable线程安全但性能较低,已少用;ConcurrentHashMap线程安全且高性能,是并发环境下的首选。三者在线程安全性与性能间各有侧重。

导言

在Java编程中,哈希表是一种非常重要的数据结构,它提供了键-值对的存储和快速检索功能。HashMap、ConcurrentHashMap和HashTable都是Java集合框架中的哈希表实现,但它们在多个方面存在显著的区别。从线程安全性到性能表现,再到内部实现机制,这三个类各有千秋。了解它们之间的区别对于选择合适的哈希表实现至关重要,特别是在多线程环境和高并发场景下。因此,本文将深入探讨HashMap、ConcurrentHashMap和HashTable之间的主要差异!

01 线程安全性

HashMap、ConcurrentHashMap和HashTable都是Java中用于存储键值对的数据结构,但它们在线程安全性方面有所不同。

1.1 HashMap是非线程安全的

它适用于单线程环境。在多线程环境下,如果多个线程同时修改HashMap,可能会引发不可预料的结果。

Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
int value = map.get("one"); // 1

1.2 ConcurrentHashMap是线程安全的

适用于多线程环境。它引入了“分段锁”的概念,允许多个修改操作并发进行,从而提高了并发性能。

Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("one", 1);
concurrentMap.put("two", 2);
int concurrentValue = concurrentMap.get("one"); // 1

1.3 HashTable也是线程安全的

所有操作都是同步的,这意味着在多线程环境下,每次只有一个线程能够执行写操作,这可能导致性能下降。

Hashtable<String, Integer> table = new Hashtable<>();
table.put("one", 1);
table.put("two", 2);
int tableValue = table.get("one"); // 1

1.4 小结

  • 如果不需要线程安全,并且追求最佳性能,应该使用HashMap。
  • 如果需要线程安全,并且预计会有高并发访问,应该使用ConcurrentHashMap。
  • 如果需要线程安全,不介意同步带来的性能开销,并且代码需要与旧代码兼容(因为HashTable是一个较老的类),可以考虑使用HashTable。不过,在大多数情况下,ConcurrentHashMap会是更好的选择。

02 继承的父类

HashMap, ConcurrentHashMap, 和 HashTable 都是实现了 Map 接口的类,用于存储键值对。但它们并没有直接继承自同一个父类,而是都实现了 Map 接口。在 Java 集合框架中,通常是通过接口来定义行为,而不是通过继承来实现功能。

下面是这三个类在继承和实现方面的主要区别:

2.1 HashMap继承自AbstractMap类

  • 继承与实现HashMap 继承自 AbstractMap 类并实现了 Map, Cloneable, 和 Serializable 接口。
  • 特点HashMap 提供了最好的性能,但不保证映射的顺序,也不提供线程安全。
  • 代码示例
import java.util.HashMap;
import java.util.Map;

public class HashMapExample {
   
    public static void main(String[] args) {
   
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);
        System.out.println(map); // 输出可能是 {one=1, two=2}(顺序不保证)
    }
}

2.2 ConcurrentHashMap继承自AbstractMap类

  • 继承与实现ConcurrentHashMap 同样继承自 AbstractMap 类并实现了 ConcurrentMap, Serializable 接口。在 Java 8 及更高版本中,它还实现了更多的接口以支持并行计算。
  • 特点ConcurrentHashMap 提供了高并发下的线程安全,并且尽可能地减少了锁竞争。
  • 代码示例
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentHashMapExample {
   
    public static void main(String[] args) {
   
        Map<String, Integer> map = new ConcurrentHashMap<>();
        map.put("one", 1);
        map.put("two", 2);
        System.out.println(map); // 输出可能是 {one=1, two=2}(顺序不保证)
    }
}

2.3 HashTable继承自Dictionary类

  • 继承与实现HashTable 继承自 Dictionary 类(一个已被废弃的类)并实现了 Map, Cloneable, Serializable 接口。然而,从 Java 2 开始,HashTable 的实现不再建议直接继承 Dictionary,因为它是一个遗留类。实际上,在查看 Java 标准库源码时,会发现 HashTable 并没有直接继承 Dictionary,而是直接实现了 Map 接口。
  • 特点HashTable 是线程安全的,但所有公共的 HashTable 方法都使用 synchronized,这可能导致在高并发情况下性能不佳。
  • 代码示例
import java.util.Hashtable;
import java.util.Map;

public class HashTableExample {
   
    public static void main(String[] args) {
   
        Map<String, Integer> table = new Hashtable<>();
        table.put("one", 1);
        table.put("two", 2);
        System.out.println(table); // 输出可能是 {one=1, two=2}(顺序不保证)
    }
}

2.4 小结

尽管 HashMap, ConcurrentHashMap, 和 HashTable 都实现了 Map 接口,但它们在内部实现、线程安全性、和性能方面有所不同。在选择使用哪一个时,应该基于的具体需求来决定。如果需要高并发下的线程安全,ConcurrentHashMap 通常是最佳选择。如果不需要线程安全,并且想要最佳性能,那么应该选择 HashMap。而 HashTable 由于其所有操作都是同步的,通常只在旧代码或特殊情况下使用。

03 对null值的处理

HashMap、ConcurrentHashMap和HashTable在处理null值时的行为是不同的。以下是关于它们如何处理null键和值的详细描述以及相关代码片段。

3.1 HashMap允许使用null作为键与值

HashMap允许使用null作为键(只能有一个)和值(可以有多个,但实际上键的唯一性通常决定了值的唯一性)。这是HashMap相对于HashTable的一个显著特点。

代码示例:

import java.util.HashMap;  

public class HashMapNullExample {
     
    public static void main(String[] args) {
     
        HashMap<String, String> hashMap = new HashMap<>();  

        // 插入null键和值  
        hashMap.put(null, "nullValue");  
        hashMap.put("key", null);  

        // 获取null键对应的值  
        String valueForNullKey = hashMap.get(null);  
        System.out.println("Value for null key: " + valueForNullKey); // 输出: nullValue  

        // 尝试覆盖null键的值  
        hashMap.put(null, "newNullValue");  
        valueForNullKey = hashMap.get(null);  
        System.out.println("New value for null key: " + valueForNullKey); // 输出: newNullValue  
    }  
}

3.2 ConcurrentHashMap允许使用null作为键和值

ConcurrentHashMap同样允许使用null作为键和值,但在多线程环境下必须小心处理以避免并发问题。ConcurrentHashMap是线程安全的,但它的设计并不是为了在每个方法上都加同步锁,而是通过在内部使用分段锁或其他并发控制机制来实现高效的并发访问。

代码示例:

import java.util.concurrent.ConcurrentHashMap;  

public class ConcurrentHashMapNullExample {
     
    public static void main(String[] args) {
     
        ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();  

        // 插入null键和值  
        concurrentHashMap.put(null, "nullValue");  
        concurrentHashMap.put("key", null);  

        // 获取null键对应的值  
        String valueForNullKey = concurrentHashMap.get(null);  
        System.out.println("Value for null key: " + valueForNullKey); // 输出: nullValue  

        // 由于ConcurrentHashMap的并发特性,这里不会直接展示并发修改的代码  
        // 但可以像使用HashMap一样使用它,只是在多线程环境下它是安全的  
    }  
}

3.2 HashTable不允许使用null作为键或值

HashTable不允许使用null作为键或值。如果尝试这样做,将会抛出NullPointerException。这是HashTable的一个严格限制,与HashMap和ConcurrentHashMap不同。

代码示例:

import java.util.Hashtable;  

public class HashTableNullExample {
     
    public static void main(String[] args) {
     
        Hashtable<String, String> hashTable = new Hashtable<>();  

        try {
     
            // 尝试插入null键,将抛出NullPointerException  
            hashTable.put(null, "nullValue");  
        } catch (NullPointerException e) {
     
            System.out.println("Cannot insert null key into Hashtable: " + e.getMessage());  
        }  

        try {
     
            // 尝试插入null值,同样将抛出NullPointerException(实际上不会,因为HashTable允许null值,但不允许null键)  
            // 注意:这里的注释是不准确的,因为HashTable实际上允许null作为值,但不允许null作为键。  
            // 然而,由于上面的代码已经因为尝试插入null键而失败,所以下面的代码实际上不会执行。  
            // 正确的做法是先插入一个有效的键,然后尝试使用这个键来存储null值。  
            hashTable.put("key", null);  
        } catch (NullPointerException e) {
     
            // 这个异常实际上不会被捕获,因为HashTable允许null值(但不允许null键)  
            System.out.println("Cannot insert null value into Hashtable: " + e.getMessage());  
        }  

        // 由于上面的代码会因为尝试插入null键而抛出异常,所以下面的代码不会执行  
        // 正确的做法是移除尝试插入null键的代码,并只测试插入null值的情况(如果需要的话)  
    }  
}

注意: 上面的HashTable示例中关于插入null值的注释是不准确的。实际上,HashTable允许使用null作为值,但是不允许使用null作为键。如果尝试使用null作为键,将会抛出NullPointerException。然而,如果先插入一个有效的键,然后使用该键来存储null值,这是完全允许的。因此,上面的代码示例中关于插入null值的部分是不正确的,并且不会导致NullPointerException。正确的做法是移除尝试插入null键的代码,并只测试插入null值的情况(如果需要的话)。但是,为了保持示例的一致性并展示HashTable对null键的处理方式,我保留了原始代码和注释,但添加了这个澄清说明。在实际应用中,应该避免向HashTable中插入null键

04 性能与场景

HashMap、ConcurrentHashMap和HashTable在性能上有所不同,这主要取决于它们的线程安全性和实现机制。以下是它们的性能对比:

4.1 HashMap

  • 性能特点:HashMap在单线程环境下通常提供最佳的性能。它不进行任何同步操作,因此没有线程安全的开销。
  • 适用场景:适用于单线程或者明确知道没有并发修改的场景。
  • 注意事项:在多线程环境下,如果多个线程同时修改HashMap,可能会导致数据不一致甚至并发修改异常(ConcurrentModificationException)。

4.2 ConcurrentHashMap

  • 性能特点:ConcurrentHashMap通过分段锁(在JDK 8之前)或其他并发控制机制(在JDK 8及之后,如使用CAS操作和synchronized)来实现线程安全,同时保持较高的并发性能。它通常比HashTable提供更好的伸缩性和性能。
  • 适用场景:适用于需要高并发读写操作的场景。
  • 注意事项:ConcurrentHashMap的设计目标是在并发环境中提供高吞吐量,而不是保证每个单独操作的快速执行。

4.3 HashTable

  • 性能特点:HashTable是线程安全的,但它的所有公共方法(如get、put和remove)都是同步的,这意味着每次只有一个线程能够执行这些方法。这种同步机制导致在并发环境中性能较差,因为即使多个线程可以并发读取,它们也必须等待锁的释放。
  • 适用场景:由于性能原因,HashTable通常不推荐在新代码中使用,除非需要兼容旧代码或者明确需要全表锁定的行为。
  • 注意事项:HashTable的性能在并发环境中可能会急剧下降,尤其是在高争用条件下。

4.4 小结

  • 单线程环境下,HashMap通常性能最好。
  • 需要线程安全时,ConcurrentHashMap通常比HashTable提供更好的性能和伸缩性。
  • HashTable由于其全表锁定的行为,通常不适用于高并发环境。

在JDK 8及以后的版本中,ConcurrentHashMap的性能得到了进一步提升,通过红黑树优化了哈希冲突的处理,以及使用更细粒度的锁来减少线程间的竞争。因此,在需要线程安全的哈希表时,ConcurrentHashMap通常是首选。

05 迭代器行为

HashMap, ConcurrentHashMap, 和 HashTable 的迭代器行为在遍历过程中有所不同,尤其是在并发修改时。下面是对这三个类迭代器行为的详细解释以及相关代码片段。

5.1 HashMap

  • 迭代器行为HashMap 的迭代器是快速失败的(fail-fast),这意味着如果在迭代过程中有其他线程修改了映射的结构(增加或删除元素),则迭代器会抛出 ConcurrentModificationException
  • 代码片段
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class HashMapIteratorExample {
   
    public static void main(String[] args) {
   
        Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
   
            Map.Entry<String, Integer> entry = iterator.next();
            System.out.println(entry.getKey() + " = " + entry.getValue());

            // 以下代码会导致 ConcurrentModificationException
            // 如果在迭代过程中修改映射
            // map.put("three", 3);
        }
    }
}

5.2 ConcurrentHashMap

  • 迭代器行为ConcurrentHashMap 的迭代器被设计为弱一致性的(weakly consistent),它不会抛出 ConcurrentModificationException。这意味着迭代器能够反映出映射在某个时间点上的状态,但如果映射在迭代过程中被修改,迭代器不一定能看到这些修改。
  • 代码片段
import java.util.concurrent.ConcurrentHashMap;
import java.util.Iterator;
import java.util.Map;

public class ConcurrentHashMapIteratorExample {
   
    public static void main(String[] args) {
   
        Map<String, Integer> map = new ConcurrentHashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
   
            Map.Entry<String, Integer> entry = iterator.next();
            System.out.println(entry.getKey() + " = " + entry.getValue());

            // ConcurrentHashMap 允许在迭代时修改映射
            map.put("three", 3);
        }
    }
}

5.3 HashTable

  • 迭代器行为HashTable 的迭代器与 HashMap 类似,也是快速失败的。如果在迭代过程中映射被修改,将会抛出 ConcurrentModificationException
  • 代码片段
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

public class HashTableIteratorExample {
   
    public static void main(String[] args) {
   
        Map<String, Integer> table = new Hashtable<>();
        table.put("one", 1);
        table.put("two", 2);

        Iterator<Map.Entry<String, Integer>> iterator = table.entrySet().iterator();
        while (iterator.hasNext()) {
   
            Map.Entry<String, Integer> entry = iterator.next();
            System.out.println(entry.getKey() + " = " + entry.getValue());

            // 以下代码会导致 ConcurrentModificationException
            // 如果在迭代过程中修改映射
            // table.put("three", 3);
        }
    }
}

5.4 小结

  • HashMapHashTable 的迭代器在检测到并发修改时会抛出异常,这是快速失败策略。
  • ConcurrentHashMap 的迭代器则设计为弱一致性的,它不会抛出异常,但可能无法看到在迭代过程中发生的所有修改。

在编写多线程代码时,应谨慎选择适合当前需求的集合类,以确保数据的完整性和一致性。如果需要在迭代过程中修改映射,并且不希望迭代器抛出异常,那么 ConcurrentHashMap 是一个合适的选择。

06 总结

HashMap、ConcurrentHashMap和HashTable是Java中常用的哈希表实现,它们在功能、性能和线程安全性方面存在显著区别。HashMap是非线程安全的,适用于单线程环境,提供最快的查询和插入操作。然而,在多线程环境下,它可能导致数据不一致。相比之下,HashTable是线程安全的,但其所有操作都是同步的,导致性能较差,尤其在高并发场景下。因此,HashTable已逐渐被淘汰。ConcurrentHashMap则是为并发场景设计的线程安全哈希表,它采用分段锁或其他并发控制机制,实现了高并发性能和伸缩性。在JDK 8及以后版本中,ConcurrentHashMap进一步优化了哈希冲突的处理和锁的粒度,减少了线程间的竞争。总之,HashMap适用于单线程环境,HashTable因性能问题不推荐使用,而ConcurrentHashMap是并发环境下的首选哈希表实现。在选择时,应根据具体需求和场景来权衡性能和线程安全性。

相关文章
|
7天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
9天前
|
安全 Java 编译器
JDK 10中的局部变量类型推断:Java编程的简化与革新
JDK 10引入的局部变量类型推断通过`var`关键字简化了代码编写,提高了可读性。编译器根据初始化表达式自动推断变量类型,减少了冗长的类型声明。虽然带来了诸多优点,但也有一些限制,如只能用于局部变量声明,并需立即初始化。这一特性使Java更接近动态类型语言,增强了灵活性和易用性。
91 53
|
8天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
5天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
20 2
|
8天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
36 1
|
1月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
29 2
|
1月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
33 2
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
52 0