想象你在银行开设账户:
为什么Java的设计者们要做出这样的限制?这背后隐藏着怎样的并发编程智慧?今天我们就来解开这个看似简单却意义重大的设计谜题。
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key", null); // 抛出NullPointerException!
map.put(null, "value"); // 同样抛出异常
特性 | HashMap | ConcurrentHashMap |
---|---|---|
允许key为null | ✅ 是 | ❌ 否 |
允许value为null | ✅ 是 | ❌ 否 |
线程安全 | ❌ 否 | ✅ 是 |
考虑这段存在竞态条件的代码:
if (!map.containsKey(key)) {
map.put(key, value); // 这两步不是原子的!
}
在ConcurrentHashMap中,如果允许value为null:
containsKey(key)
返回falsemap.put(key, null)
// ConcurrentHashMap的get实现片段
V get(Object key) {
if ((e = tabAt(tab, (n - 1) & h)) != null) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val; // 快速返回
}
}
禁止null可以:
Doug Lea(ConcurrentHashMap作者)的设计理念:
“在并发编程中,显式优于隐式,确定优于模糊”
null带来的二义性:
map.get(key)
返回null时:
这种模糊性在并发环境下是危险的
ConcurrentHashMap<String, User> cache = new ConcurrentHashMap<>();
// 线程A
User user = cache.get(userId);
if (user == null) {
user = loadFromDB(userId); // 耗时操作
cache.put(userId, user);
}
// 线程B同时执行
cache.put(userId, null); // 如果允许null...
结果:导致缓存穿透,所有请求打到数据库
ConcurrentHashMap<String, String> config = new ConcurrentHashMap<>();
// 线程A认为"未配置"
if (config.get("timeout") == null) {
useDefaultTimeout();
}
// 线程B却表示"配置为null即无超时"
config.put("timeout", null);
结果:系统行为不一致
public static final Object NULL_PLACEHOLDER = new Object();
ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
map.put("key", NULL_PLACEHOLDER); // 代替null
// 取值时
Object value = map.get("key");
if (value == NULL_PLACEHOLDER) {
// 处理null等价逻辑
}
ConcurrentHashMap<String, Optional<String>> map = new ConcurrentHashMap<>();
map.put("key", Optional.empty()); // 表示null
// 取值
Optional<String> opt = map.get("key");
String realValue = opt.orElse("default");
class NullableConcurrentHashMap<K,V> {
private final ConcurrentHashMap<K, Optional<V>> delegate = new ConcurrentHashMap<>();
public void putNullable(K key, V value) {
delegate.put(key, Optional.ofNullable(value));
}
public V getNullable(K key) {
return delegate.getOrDefault(key, Optional.empty()).orElse(null);
}
}
ConcurrentHashMap的选择:
// put方法的null检查(JDK11源码)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// ...其余逻辑...
}
哲学:在问题发生前严格约束,避免后续复杂判断
容器类 | 允许null键值 | 设计考量 |
---|---|---|
ConcurrentHashMap | ❌ | 避免并发歧义 |
ConcurrentSkipListMap | ❌ | 保持有序性约束 |
CopyOnWriteArrayList | ✅ | 读多写少场景风险低 |
ConcurrentHashMap对null的拒绝告诉我们:
就像交通规则:
记住:在并发世界,显式的空值处理比隐式的null更有价值!