1. 线程安全性 (核心区别):
-
HashMap(非线程安全):-
问题: 在多线程环境下,如果多个线程同时对
HashMap进行修改操作(例如put()、remove()、clear()等),会导致数据不一致,甚至可能导致HashMap的内部结构损坏,例如出现链表循环,最终导致死循环。 -
原因:
HashMap的内部操作(例如插入元素、删除元素、扩容等)都没有进行任何线程同步。
-
-
ConcurrentHashMap(线程安全):- 保证:
ConcurrentHashMap通过复杂的锁机制保证了并发访问的安全性,多个线程可以同时对ConcurrentHashMap进行读写操作,而不会导致数据不一致。
- 保证:
2. 数据结构 & 锁机制 (底层实现):
| Feature | HashMap | ConcurrentHashMap (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 独立扩容。
- 分段锁 (Segment Lock):
ConcurrentHashMap(JDK 1.8 及之后):- CAS +
synchronized: JDK 1.8 及之后,ConcurrentHashMap放弃了分段锁,而是采用 CAS (Compare and Swap) +synchronized的方式来保证并发安全。 - 锁粒度: 锁的粒度降低到节点级别 (Node level)。
- Node 状态: 每个哈希桶的头节点 (Node) 有几种状态:
null: 表示该哈希桶为空。FWD: 表示该哈希桶正在进行扩容。RESERVED: 表示该哈希桶已经被一个线程锁定。- 其他值: 表示该哈希桶的头节点的哈希值。
- 写操作:
- 使用 CAS 操作尝试将新节点添加到哈希桶的头部。
- 如果 CAS 操作失败(说明有其他线程正在修改该哈希桶),则使用
synchronized关键字对该哈希桶的头节点进行加锁。 - 加锁成功后,再次尝试将新节点添加到哈希桶。
- 读操作: 读操作通常不需要加锁(除非正在进行扩容或遇到
FWD节点)。 - 扩容: 支持多线程并发扩容,提高扩容效率。
- CAS +
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 值 (单线程) | HashMap | ConcurrentHashMap 不允许 null 键和 null 值。 |
| 迭代过程中不允许修改 (Fail-fast) | HashMap | ConcurrentHashMap 的迭代器是 Fail-safe 的。 |
对 size()/isEmpty() 结果要求绝对精确 | 谨慎选择,通常 ConcurrentHashMap 不保证 | ConcurrentHashMap 的 size()/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());
// }
}
}
总结:
HashMap 和 ConcurrentHashMap 是 Java 中两种常用的哈希表实现。 HashMap 适用于单线程环境,性能较高,但不是线程安全的。 ConcurrentHashMap 适用于多线程环境,提供线程安全的并发访问,但牺牲了一定的性能。 在选择使用哪种哈希表时,需要根据具体的应用场景和需求进行权衡。
1200

被折叠的 条评论
为什么被折叠?



