多线程:三大集合类
在Java开发中,HashTable、HashMap和ConcurrentHashMap是三个常用的键值对存储集合,它们在面试和实际开发中都扮演着重要角色。
1.基本概念
HashTable
HashTable是Java早期(JDK 1.0)提供的键值对存储结构,它是一个线程安全的哈希表实现。
// HashTable基本用法
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("key1", 1);
hashtable.put("key2", 2);
Integer value = hashtable.get("key1");
HashMap
HashMap在JDK 1.2中引入,提供了与HashTable类似的功能,但不是线程安全的,性能更好。
// HashMap基本用法
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("key1", 1);
hashMap.put("key2", 2);
Integer value = hashMap.get("key1");
ConcurrentHashMap
ConcurrentHashMap在JDK 1.5中引入,旨在提供线程安全且高性能的哈希表实现。
// ConcurrentHashMap基本用法
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key1", 1);
concurrentMap.put("key2", 2);
Integer value = concurrentMap.get("key1");
2. 线程安全性对比
HashTable的线程安全实现
HashTable通过在所有公共方法上添加`synchronized`关键字来实现线程安全:
// HashTable的同步机制(简化版)
public synchronized V put(K key, V value) {
// 实现逻辑
}
public synchronized V get(Object key) {
// 实现逻辑
}
这种粗粒度的锁机制导致性能瓶颈,因为整个表被锁定,同一时间只能有一个线程操作。
HashMap的线程不安全特性
HashMap不是线程安全的,在多线程环境下可能出现问题:
// 多线程下HashMap的问题示例
public class HashMapThreadUnsafe {
public static void main(String[] args) throws InterruptedException {
Map<String, Integer> map = new HashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i * 2);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 可能出现各种异常或数据不一致
}
}
ConcurrentHashMap的线程安全实现
ConcurrentHashMap使用更精细的锁机制:
JDK 1.7及之前:使用分段锁(Segment)
// JDK 1.7的分段锁概念
static final class Segment<K,V> extends ReentrantLock {
// 每个Segment管理一部分哈希桶
}
JDK 1.8及之后:使用CAS + synchronized
// JDK 1.8使用Node和CAS
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
3. 性能分析
单线程性能
- HashMap:性能最佳,无同步开销
- ConcurrentHashMap:次之,有少量CAS开销
- HashTable:性能最差,同步开销大
多线程性能
在多线程环境下,性能对比发生显著变化:
// 性能测试示例
public class MapPerformanceTest {
private static final int THREAD_COUNT = 10;
private static final int OPERATION_COUNT = 100000;
public static void main(String[] args) throws InterruptedException {
// HashMap(线程不安全,仅作对比)
// Map<String, Integer> map = new HashMap<>();
// HashTable性能测试
Map<String, Integer> hashtable = new Hashtable<>();
testPerformance(hashtable, "Hashtable");
// ConcurrentHashMap性能测试
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
testPerformance(concurrentMap, "ConcurrentHashMap");
}
private static void testPerformance(Map<String, Integer> map, String name)
throws InterruptedException {
long startTime = System.currentTimeMillis();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < OPERATION_COUNT; j++) {
String key = "key-" + Thread.currentThread().getId() + "-" + j;
map.put(key, j);
map.get(key);
}
});
threads.add(thread);
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println(name + " 耗时: " + (endTime - startTime) + "ms");
}
}
测试结果通常显示:
- ConcurrentHashMap在多线程环境下性能明显优于HashTable
- 随着线程数增加,性能差距更加明显
4. 空值处理差异
HashMap
允许null键和null值:
HashMap<String, String> map = new HashMap<>();
map.put(null, "null key"); // 允许
map.put("key", null); // 允许
HashTable
不允许null键或null值:
Hashtable<String, String> table = new Hashtable<>();
table.put(null, "value"); // 抛出NullPointerException
table.put("key", null); // 抛出NullPointerException
ConcurrentHashMap
不允许null键或null值:
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put(null, "value"); // 抛出NullPointerException
concurrentMap.put("key", null); // 抛出NullPointerException
设计原因:
- HashTable和ConcurrentHashMap在并发环境下,无法明确区分"键不存在"和"键对应的值为null"
- HashMap在单线程环境下没有这个问题
5. 内部实现细节
HashMap的内部结构(JDK 1.8+)
// 数组 + 链表/红黑树
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
当链表长度超过8且数组长度大于64时,链表转换为红黑树。
ConcurrentHashMap的内部结构(JDK 1.8+)
// 使用volatile和CAS保证可见性和原子性
transient volatile Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
6. 扩容机制对比
HashMap扩容
- 默认初始容量:16
- 负载因子:0.75
- 扩容:容量翻倍
ConcurrentHashMap扩容
- 更复杂的扩容机制,支持并发扩容
- 多个线程可以协助完成扩容过程
7. 使用场景建议
使用HashMap的场景
- 单线程环境
- 不需要线程安全
- 需要最高性能
- 需要存储null键或null值
// 缓存实现示例(单线程)
public class SimpleCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
public void put(K key, V value) {
cache.put(key, value);
}
public V get(K key) {
return cache.get(key);
}
}
使用ConcurrentHashMap的场景
- 高并发环境
- 需要线程安全
- 对性能要求较高
- 不需要存储null值
// 线程安全的缓存实现
public class ConcurrentCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
public void put(K key, V value) {
cache.put(key, value);
}
public V get(K key) {
return cache.get(key);
}
// 使用computeIfAbsent实现原子操作
public V computeIfAbsent(K key, Function<K, V> mappingFunction) {
return cache.computeIfAbsent(key, mappingFunction);
}
}
使用HashTable的场景
- 遗留系统维护
- 需要与旧代码兼容
- 并发要求不高的场景
1. 单线程环境:优先使用HashMap
2. 高并发环境:优先使用ConcurrentHashMap
3. 新项目:避免使用HashTable
4. 缓存null值:考虑使用Optional包装