Java 中 ConcurrentHashMap 的 get 方法是否需要加锁?
·
Java 中 ConcurrentHashMap 的 get 方法不需要加锁,它是一个完全无锁的操作。
核心原理
Java 1.7 实现
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
// 计算 hash 值
int h = hash(key);
// 定位到对应的 Segment
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 遍历链表查找(无锁)
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
Java 1.8 实现
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算 hash 值
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 检查第一个节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 处理红黑树或扩容状态
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表查找
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
为什么 get 不需要加锁?
1. volatile 关键字保证可见性
// Java 1.7 中的 HashEntry
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value; // volatile 保证可见性
volatile HashEntry<K,V> next;
}
// Java 1.8 中的 Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // volatile 保证可见性
volatile Node<K,V> next;
}
2. 内存屏障和原子操作
// Java 1.8 使用 Unsafe 类进行原子操作
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
并发场景下的安全性
读-写并发
public class ConcurrentHashMapGetExample {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// 线程1:写操作(需要加锁)
Thread writer = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, "value" + i);
}
});
// 线程2:读操作(无锁)
Thread reader = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
String value = map.get("key" + i); // 无锁读取
System.out.println("Read: " + value);
}
});
writer.start();
reader.start();
writer.join();
reader.join();
}
}
读-读并发
// 多个线程同时读取完全安全
public class MultiReaderExample {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("sharedKey", "sharedValue");
// 创建多个读线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
String value = map.get("sharedKey"); // 并发读取,无需同步
System.out.println(Thread.currentThread().getName() + ": " + value);
}
}, "Reader-" + i).start();
}
}
}
与 HashMap 的对比
HashMap 的 get 方法
// HashMap 在多线程环境下可能产生问题
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 没有 volatile 保证,多线程下可能读到过期数据
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 可能因为扩容导致死循环等问题
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 链表遍历可能遇到并发修改问题
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
技术实现细节
1. 可见性保证
// volatile 变量的内存语义
class VolatileExample {
volatile int value;
// 写操作会刷新到主内存
void write() {
value = 42; // 立即对其他线程可见
}
// 读操作会从主内存读取最新值
int read() {
return value; // 总是读取最新值
}
}
2. 内存屏障
// ConcurrentHashMap 使用的内存屏障
public class MemoryBarrierExample {
// LoadLoad 屏障:确保当前读取在后续读取之前完成
// StoreStore 屏障:确保当前写入在后续写入之前对其他处理器可见
// LoadStore 屏障:确保读取在后续存储之前完成
// StoreLoad 屏障:最重的屏障,确保所有写入对其他处理器可见
}
性能优势
基准测试示例
public class PerformanceBenchmark {
public static void main(String[] args) {
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
HashMap<String, String> hashMap = new HashMap<>();
// 填充数据
for (int i = 0; i < 100000; i++) {
concurrentMap.put("key" + i, "value" + i);
hashMap.put("key" + i, "value" + i);
}
// 多线程读性能测试
testReadPerformance(concurrentMap, "ConcurrentHashMap");
testReadPerformance(hashMap, "HashMap");
}
static void testReadPerformance(Map<String, String> map, String name) {
long start = System.currentTimeMillis();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
map.get("key" + (j % 100000));
}
});
threads.add(t);
t.start();
}
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(name + " read time: " + (end - start) + "ms");
}
}
特殊情况处理
扩容期间的读取
// Java 1.8 中扩容期间的读取仍然安全
public V get(Object key) {
// ...
else if (eh < 0) {
// 如果节点处于特殊状态(如扩容中)
// 调用节点的 find 方法进行查找
return (p = e.find(h, key)) != null ? p.val : null;
}
// ...
}
红黑树查找
// 红黑树的查找也是无锁的
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
if (((s = lockState) & (WAITER|WRITER)) != 0) {
// 如果有写操作在进行,降级为链表查找
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
else if (U.compareAndSetInt(this, LOCKSTATE, s,
s + READER)) {
// 无锁的红黑树查找
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
总结
ConcurrentHashMap 的 get 方法不需要加锁的原因:
- volatile 关键字:保证值的可见性
- final 字段:键和哈希值不可变
- 内存屏障:通过 Unsafe 类保证内存一致性
- 原子操作:使用 CAS 等原子操作
- 设计优化:读写分离的设计理念
这种无锁设计使得 ConcurrentHashMap 在读多写少的场景下具有极高的性能优势。
更多推荐

所有评论(0)