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 方法不需要加锁的原因:

  1. volatile 关键字:保证值的可见性
  2. final 字段:键和哈希值不可变
  3. 内存屏障:通过 Unsafe 类保证内存一致性
  4. 原子操作:使用 CAS 等原子操作
  5. 设计优化:读写分离的设计理念

这种无锁设计使得 ConcurrentHashMap 在读多写少的场景下具有极高的性能优势。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐