HashMap、Hashtable和ConcurrentHashMap的区别
- ✔️ 三者区别
- ✔️ 线程安全方面
- ✔️继承关系方面
- ✔️ 允不允许null值方面
- ✔️为什么ConcurrentHashMap不允许null值?
- ✔️ 默认初始容量和扩容机制
- ✔️遍历方式的内部实现上不同
✔️ 三者区别
✔️ 线程安全方面
HashMap是非线程安全的。
Hashtable 中的方法是同步的,所以它是线程安全的。
ConcurrentHashMap
在JDK 1.8之前使用分段锁保证线程安全,ConcurrentHashMap
默认情况下将hash
表分为16个桶(分片),在加锁的时候,针对每个单独的分片进行加锁,其他分片不受影响。锁的粒度更细,所以他的性能更好。
ConcurrentHashMap
在JDK 1.8中,采用了一种新的方式来实现线程安全,即使用了CAS+synchronized
,这个实现被称为"分段锁"的变种,也被称为"锁分离”,它将锁定粒度更细,把锁的粒度从整个Map降低到了单个桶。
看一段代码,HashMap、Hashtable和ConcurrentHashMap在多线程环境中的行为:
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
public class ThreadSafeHashMapComparison {
public static void main(String[] args) {
// 1. HashMap - 非线程安全的示例
HashMapExample(new HashMap<>());
// 2. Hashtable - 线程安全的示例
HashtableExample(new Hashtable<>());
// 3. ConcurrentHashMap - 线程安全的示例,性能更好
ConcurrentHashMapExample(new ConcurrentHashMap<>());
}
public static void HashMapExample(HashMap<Integer, String> map) {
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
// 启动两个线程同时修改map
new Thread(() -> {
map.put(4, "Four");
}).start();
new Thread(() -> {
map.remove(2);
}).start();
}
public static void HashtableExample(Hashtable<Integer, String> hashtable) {
hashtable.put(1, "One");
hashtable.put(2, "Two");
hashtable.put(3, "Three");
// 启动两个线程同时修改hashtable
new Thread(() -> {
hashtable.put(4, "Four");
}).start();
new Thread(() -> {
hashtable.remove(2);
}).start();
}
public static void ConcurrentHashMapExample(ConcurrentHashMap<Integer, String> concurrentHashMap) {
concurrentHashMap.put(1, "One");
concurrentHashMap.put(2, "Two");
concurrentHashMap.put(3, "Three");
// 启动两个线程同时修改concurrentHashMap
new Thread(() -> {
concurrentHashMap.put(4, "Four");
}).start();
new Thread(() -> {
concurrentHashMap.remove(2);
}).start();
}
}
趁热打铁,再来看一段代码,使用Java中的ReentrantLock
和 Condition
来 实现一个线程安全的、可扩展的 HashMap
。这个示例中,我们还将展示如何处理更复杂的并发情况,如多个线程同时尝试修改相同的键。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadSafeHashMap<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public V put(K key, V value) {
lock.lock();
try {
// 等待当前线程获取锁后,再执行下面的代码
condition.await();
// 检查键是否已经存在,如果存在则更新值,否则插入新键值对
return map.merge(key, value, (oldValue, newValue) -> newValue);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public V remove(K key) {
lock.lock();
try {
// 等待当前线程获取锁后,再执行下面的代码
condition.await();
return map.remove(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ThreadSafeHashMap<Integer, String> threadSafeHashMap = new ThreadSafeHashMap<>();
threadSafeHashMap.put(1, "One");
threadSafeHashMap.put(2, "Two");
threadSafeHashMap.put(3, "Three");
// 启动两个线程同时修改map,其中一个线程尝试更新已存在的键,另一个线程尝试删除一个键
new Thread(() -> {
threadSafeHashMap.put(4, "Four"); // 插入新键值对
}).start();
new Thread(() -> {
threadSafeHashMap.remove(2); // 删除键值对(2,"Two")
}).start();
}
}
✔️继承关系方面
HashTable是基于陈旧的
Dictionary
类继承来的。
HashMap
继承的抽象类AbstractMap
实现了Map
接口。
ConcurrentHashMap
同样继承了抽象类AbstractMap
,并且实现了ConcurrentMap
接口。
接下来,我们通过代码来展示它们在继承关系方面的区别:
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
public class HashMapVsHashtableVsConcurrentHashMap {
public static void main(String[] args) {
// 创建一个HashMap实例
HashMap<String, Integer> hashMap = new HashMap<>();
System.out.println("HashMap继承关系: " + hashMap.getClass().getSuperclass());
// 创建一个Hashtable实例
Hashtable<String, Integer> hashtable = new Hashtable<>();
System.out.println("Hashtable继承关系: " + hashtable.getClass().getSuperclass());
// 创建一个ConcurrentHashMap实例
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
System.out.println("ConcurrentHashMap继承关系: " + concurrentHashMap.getClass().getSuperclass());
}
}
运行上面的代码,输出结果将会显示这三个类在继承关系上的不同。输出结果如下:
HashMap
继承自AbstractMap
。Hashtable
继承自Dictionary
和Hashtable
。这是因为Hashtable
是遗留类,设计用于Java 1.0,而Dictionary
是它的超类。ConcurrentHashMap
也继承自AbstractMap
,与HashMap
类似。这是因为它的设计目标是为了提供线程安全的哈希表,而不需要额外的线程安全机制。
✔️ 允不允许null值方面
HashTable
中,key
和value
都不允许出现null 值,否则会抛出NullPointerException
异常。
HashMap
中,null 可以作为键或者值都可以。
ConcurrentHashMap
中,key
和value
都不允许为null。
✔️为什么ConcurrentHashMap不允许null值?
我们知道,ConcurrentHashMap
在使用时,和 HashMap
有一个比较大的区别,那就是HashMap
中,null
可以作为键或者值都可以。而在 ConcurrentHashMap
中,key
和value
都不允许为null
。
那么,为什么呢? 为啥ConcurrentHashMap要设计成这样的呢?
关于这个问题,其实最有发言权的就是ConcurrentHashMap的作者-Doug Lea
大家看一个截图吧。因为原文地址现在不知道怎么搞的打不开了。一张截图大家凑合看着吧。
主要意思就是说 :
ConcurrentMap
(如 ConcurrentHashMap
、ConcurrentSkipListMap
) 不允许使用 null 值的主要原因是,在非并发的Map中(如HashMap),是可以容忍模糊性 (二义性)的,而在并发Map中是无法容忍的。
假如说,所有的 Map 都支持 null 的话,那么 map.get(key) 就可以返回 null ,但是,这时候就会存在一个不确定性,当你拿到null的时候,你是不知道他是因为本来就存了一个 null 进去还是说就是因为没找到而返回了null。
在HashMap中,因为它的设计就是给单线程用的,所以当我们map.get(key)返回nul的时候,我们是可以通过map.contains(key)检查来进行检测的,如果它返回true,则认为是存了一个null,否则就是因为没找到而返回了null。
但是,像ConcurrentHashMap,它是为并发而生的,它是要用在并发场景中的,当我们map.get(key)返回null的时候,是没办法通过map.contains(key)检查来准确的检测,因为在检测过程中可能会被其他线程所修改,而导致检测结果并不可靠。
所以,为了让 ConcurrentHashMap
的语义更加准确,不存在二义性的问题,他就不支持null。
✔️ 默认初始容量和扩容机制
HashMap的默认初始容量为16,默认的加载因子为0.75,即当HashMap中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并将原来的元素重新分配到新的桶中。
Hashtable,默认初始容量为11,默认的加载因子为0.75,即当Hashtable中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍加1,并将原来的元素重新分配到新的桶中。
ConcurrentHashMap
,默认初始容量为16,默认的加载因子为0.75,即当ConcurrentHashMap
中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并会采用分段锁机制,将 ConcurrentHashMap
分为多个段(segment),每个段独立进行扩容操作,避免了整个ConcurrentHashMap
的锁竞争。
✔️遍历方式的内部实现上不同
HashMap
使用 EntrySet
进行遍历,即先获取到 HashMap
中所有的键值对(Entry),然后遍历Entry集合。支持 fail-fast
,也就是说在遍历过程中,若 HashMap
的结构被修改(添加或删除元素),则会抛出ConcurrentModificationException
,如果只需要遍历 HashMap
中的 key
或value
,可以使用KeySet
或Values
来遍历。
Hashtable
使用Enumeration
进行遍历,即获取Hashtable中所有的key,然后遍历key集合。遍历过程中,Hashtable
的结构发生变化时,Enumeration
会失效。
ConcurrentHashMap
使用分段锁机制,因此在遍历时需要注意,遍历时ConcurrentHashMap
的某人段被修改不会影响其他段的遍历。可以使用EntrySet
、KeySet或Values
来遍历ConcurrentHashMap
,其中EntrySet遍历时效率最高。遍历过程中,ConcurrentHashMap
的结构发生变化时,不会抛出ConcurrentModificationException
异常,但是在遍历时可能会出现数据不一致的情况,因为遍历器仅提供了弱一致性保障。
以下是一个8行4列的表格:
特性/集合类 | HashMap | Hashtable | ConcurrentHashMap |
---|---|---|---|
线程安全 | 否 | 是,基于方法锁 | 是,基于分段锁 |
继承关系 | AbstractMap | Dictionary | AbstractMap,ConcurrentMap |
允许null值 | K-V都允许 | K-V都不允许 | K-V都不允许 |
默认初始容量 | 16 | 11 | 16 |
默认加载因子 | 0.75 | 0.75 | 0.75 |
扩容后容量 | 原来的两倍 | 原来的两倍+1 | 原来的两倍 |
是否支持fail-fast | 支持 | 不支持 | fail-safe |