粗粒度锁怎么优化细化拆分?

访客 性能优化 2

本文目录导读:

  1. 核心原则
  2. 具体优化拆分方法(按复杂度排序)
  3. 警惕拆分陷阱
  4. 总结与推荐策略

这是一个非常经典且核心的并发编程问题,粗粒度锁(比如给整个方法或整个数据结构加一把大锁)实现简单、不易出错,但会严重限制并发性能,将其优化细化为细粒度锁,核心思路是“锁尽可能少的数据,让不相关的操作并行执行”

下面是一个系统性的优化拆分思路和具体实践方法,从简单到复杂。

核心原则

  1. 锁的粒度与数据关联性:锁保护的数据集合越小,锁的粒度越细。
  2. 锁的持有时间:锁持有时间越短,竞争越小,不要在持锁期间做I/O操作、复杂计算或调用外部不可控服务。
  3. 锁的争用程度:频繁被争用的锁是瓶颈,需要优先拆分。

具体优化拆分方法(按复杂度排序)

读写锁分离 (ReadWriteLock / StampedLock)

适用场景:读操作远多于写操作(典型如缓存、配置中心、读多写少的计数器)。

  • 问题:粗粒度锁会阻塞所有读线程,即使它们之间并不冲突。

  • 优化:允许多个读线程并发访问数据,写线程在写时独占。

  • 代码示例 (Java)

    // 粗粒度:使用 synchronized
    public synchronized Object get(String key) { ... }
    public synchronized void put(String key, Object val) { ... }
    // 细粒度优化:使用 ReentrantReadWriteLock
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    public Object get(String key) {
        readLock.lock();
        try {
            // 读数据
        } finally {
            readLock.unlock();
        }
    }
    public void put(String key, Object val) {
        writeLock.lock();
        try {
            // 写数据
        } finally {
            writeLock.unlock();
        }
    }

分段锁 (Lock Striping)

适用场景:大型、可被键值(Key)或索引(Index)拆分的数据结构,如 ConcurrentHashMap

  • 问题:给整个Map加锁,一次只能一个线程操作。

  • 优化:将数据分成若干段(Segments或Buckets),每个段有自己的锁,线程操作某个Key时,只锁定该Key所在的段,不同的段可以完全并发。

  • 实现方式

    • 数组 + 链表/红黑树:如 ConcurrentHashMap 的桶(Bucket)级别锁。
    • 哈希取模int segmentIndex = key.hashCode() % NUM_OF_SEGMENTS,然后只锁定对应的 segmentLocks[segmentIndex]
  • 代码示例 (简化版伪代码)

    // 粗粒度:整个Map一把锁
    class CoarseMap<K,V> {
        private final Object lock = new Object();
        Map<K,V> map = new HashMap<>();
        V put(K k, V v) { synchronized(lock) { return map.put(k, v); } }
    }
    // 细粒度优化:分段锁
    class StripedMap<K,V> {
        private final Object[] locks;
        private final Map<K,V>[] segments;
        StripedMap(int concurrencyLevel) {
            locks = new Object[concurrencyLevel];
            segments = new Map[concurrencyLevel];
            for (int i = 0; i < concurrencyLevel; i++) {
                locks[i] = new Object();
                segments[i] = new HashMap<>();
            }
        }
        V put(K k, V v) {
            int hash = k.hashCode();
            int segmentIndex = hash & (locks.length - 1); // 取模
            synchronized (locks[segmentIndex]) { // 只锁定该段
                return segments[segmentIndex].put(k, v);
            }
        }
    }

热点分离 (Hotspot Stripping / 剥离热点)

适用场景:某些特定数据(如一个全局计数器、一个热门商品的库存)被大量线程频繁修改。

  • 问题:即使使用了读写锁或分段锁,但某个特定的“热点”数据仍然会被大量线程争用。

  • 优化

    1. 计数器拆分:将一个全局 AtomicLong counter 拆分成 AtomicLong[] counters,线程更新时选择一个随机或基于线程ID的计数器(如 counters[threadId % N]),汇总时再累加。
    2. 库存拆分:将热门商品的1000件库存拆分成N个“分库存”槽位(库存分片),每个槽位有1000/N件库存和自己的锁,用户下单时随机选择一个槽位扣减,失败则重试另一个槽位。
  • 代码示例 (计数器拆分)

    // 粗粒度:一个全局 synchronized 计数器
    class CoarseCounter {
        long count = 0;
        synchronized long increment() { return ++count; }
    }
    // 细粒度优化:拆分成多个槽位,近似于 LongAdder 的设计
    class StripedCounter {
        private final AtomicLong[] counters;
        StripedCounter(int concurrencyLevel) {
            counters = new AtomicLong[concurrencyLevel];
            for (int i = 0; i < concurrencyLevel; i++) {
                counters[i] = new AtomicLong(0);
            }
        }
        long increment() {
            int index = ThreadLocalRandom.current().nextInt(counters.length);
            return counters[index].incrementAndGet(); // 无锁CAS,非常高效
        }
        long sum() {
            long sum = 0;
            for (AtomicLong c : counters) {
                sum += c.get();
            }
            return sum;
        }
    }

无锁数据结构 (Lock-Free / CAS)

适用场景:对单个变量或简单结构进行更新,且争用不极端的情况(极端争用仍需考虑拆分成多个槽位)。

  • 优化:用 AtomicInteger, AtomicReference, LongAdder 等原子变量替代 synchronized,更高级的包括 StampedLock(乐观读)、ConcurrentLinkedQueue 等。
  • 优势:无锁,不涉及线程上下文切换和阻塞,性能极高。
  • 注意:CAS自旋在极高争用下可能浪费CPU,此时分段锁可能更优。

缩小锁范围 (减少锁持有时间)

适用场景:所有锁场景。

  • 问题:在锁内执行了昂贵的非必要操作(如IO、类加载、对象创建)。

  • 优化只在锁内完成最核心的共享数据访问,将耗时操作移到锁外。

  • 代码示例

    // 粗粒度:整个方法加锁
    public synchronized String loadFromCache(String key) {
        String result = cache.get(key);
        if (result == null) {
            result = loadFromDatabase(key); // 非常慢,长时间持锁
            cache.put(key, result);
        }
        return result;
    }
    // 细粒度优化:双检锁(Double-Checked Locking),只在真正需要访问缓存时加锁
    public String loadFromCache(String key) {
        String result = cache.get(key); // 不加锁读
        if (result == null) {
            // 此时加锁
            synchronized (this) {
                // 再次检查,防止其他线程已经写入(双检)
                result = cache.get(key);
                if (result == null) {
                    result = loadFromDatabase(key); // 加锁期间只做数据库加载
                    cache.put(key, result);
                }
            }
        }
        return result;
    }

    更优的实践是使用 ConcurrentHashMap.computeIfAbsent() 直接内置了高效的单例逻辑。

警惕拆分陷阱

  1. 死锁风险:当需要同时获取多个细粒度锁时,必须保证所有线程获取锁的顺序一致(全局顺序锁),否则极易死锁。
    • :操作A需要锁对象1和2,操作B需要锁对象2和1,如果A拿到1,B拿到2,就死锁了,解决方法:按对象ID排序后加锁。
  2. 并发度 < 预期:分段数太少,仍会竞争;分段数太多,锁占用内存且增加哈希计算开销,通常分段数设为 16 * N(N为CPU核心数)或根据实际争用情况调整。
  3. 复合操作原子性破坏:拆分锁后,原本一个原子操作(如“先检查再更新”、“转账-扣减A余额&增加B余额”)无法在单个锁内完成,需要额外的机制来保证原子性(如 compareAndSet 循环、全局时序锁或版本号)。
  4. 数据一致性复杂度:分段锁或热点分离后,一致性模型从“强一致性”退化为“最终一致性”或“基于快照的一致性”,例如库存分片可能存在短暂的“超卖”风险,需要业务上容忍或用更复杂的协议(如两阶段提交)。

总结与推荐策略

场景 粗粒度锁 推荐细化方案
读多写少 读写都加 synchronized 读写锁 (ReadWriteLock) 或乐观锁 (StampedLock)
大型哈希表 整个 HashMap 一把锁 分段锁 (模仿 ConcurrentHashMap
全局计数器 synchronized 递增 拆分多个槽位 (LongAdderStripedCounter)
缓存加载 整个方法加锁,包括DB调用 双检锁 + 缩小锁范围,或 ConcurrentHashMap.computeIfAbsent()
多个对象操作 一个全局大锁保护所有对象 对象级锁,但注意获取多个锁时必须保证锁顺序(如按ID排序)
极度高频争用 所有操作被一把锁阻塞 考虑 Lock-Free 数据结构 (CAS)或 最终一致性(业务容忍)+ 队列

最终建议: 不要一开始就追求极致的细粒度。正确的姿势是:先用粗粒度锁保证正确性,然后通过性能分析工具(Profiler)锁定真正的锁竞争热点,最后只针对热点进行上述的针对性细化拆分。 过度设计细粒度锁可能引入死锁和复杂性,而收益却寥寥。

标签: 锁拆分

抱歉,评论功能暂时关闭!