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
类似。这是因为它的设计目标是为了提供线程安全的哈希表,而不需要额外的线程安全机制。
HashTable
中,key
和value
都不允许出现null 值,否则会抛出NullPointerException
异常。
HashMap
中,null 可以作为键或者值都可以。
ConcurrentHashMap
中,key
和value
都不允许为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 |