一.Hashtable
Hashtable 是 Java 早期版本(JDK 1.0)就提供的一个键值对存储的集合类。它实现了Dictionary接口,是一个线程安全的哈希表。它的键和值都不允许为null。
Hashtable是线程安全的,Hashtable通过直接在方法中加 synchronized 来保证线程安全,太过简单粗暴,在单线程比不过HashMap,在多线程比不过ConcurrentHashMap,属于上古版本的东西,现在都不用了。
二.HashMap
HashMap 是 Java 1.2 引入的一个键值对存储的集合类,它实现了Map接口。它允许null键和null值。
它不是线程安全的,但是在单线程环境下,它的性能比 Hashtable 好。
在多线程环境下线程不安全的原因:
1)多线程下扩容会死循环。JDK1.7 中的 HashMap 使用的是头插法插入元素,在多线程的环境下,扩容的时候就有可能导致出现环形链表,造成死循环。不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序。
2)多线程的 put 可能会导致元素的丢失。因为计算出来的位置可能会被其他线程的 put 覆盖。本来哈希冲突是应该用链表的,但多线程时由于没有加锁,相同位置的元素可能就被干掉了。
3)put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出阈值而导致出现扩容,线程 2 此时执行 get,就有可能出现这个问题。因为线程 1 执行完 table = newTab 之后,线程 2 中的 table 此时也发生了变化,此时去 get 的时候当然会 get 到 null 了,因为元素还没有转移。
三.ConcurrentHashMap
ConcurrentHashMap 是 Java 1.5 引入的一个线程安全的哈希表实现,它也是实现了Map接口。它在多线程环境下提供了高效的并发读写操作。它不允许null键和null值。
ConcurrentHashMap 在 JDK 1.7及以前使用的分段锁机制整个 Map 被分为若干段,每个段都可以独立地加锁。
ConcurrentHashMap 在JDK 1.8及以上版本使用的是桶锁(结合CAS和synchronized)。
对于读操作,通常不需要加锁,可以直接读取,ConcurrentHashMap 内部使用了 volatile 变量来保证内存可见性。
对于写操作,ConcurrentHashMap 使用 CAS 操作来实现无锁的更新,这是一种乐观锁的实现,因为它假设没有冲突发生,在实际更新数据时才检查是否有其他线程在尝试修改数据,如果有,采用悲观的锁策略,如 synchronized 代码块来保证数据的一致性。