1. 引言

本文将深入探讨一种提升并发性能的经典技术——锁分段(Lock Striping)。它是一种实现细粒度同步的模式,能够在保证线程安全的同时,显著提升多线程环境下对数据结构操作的吞吐量。

我们知道,在高并发场景下,粗粒度的锁机制往往会成为性能瓶颈。而 Lock Striping 正是为了解决这一问题而生。通过将锁的粒度从整个数据结构细化到“段”或“桶”,多个线程可以并行操作不同的段,从而减少竞争,提高并发效率。

2. 问题背景

HashMap 本身是非线程安全的,多线程同时读写可能导致数据不一致甚至结构破坏(如死循环)。✅

为解决此问题,常见的做法有:

  • 使用 Collections.synchronizedMap() 包装
  • 直接使用 Hashtable

⚠️ 这两种方式虽然实现了线程安全,但采用的是 粗粒度同步(coarse-grained synchronization) ——即整个 Map 被一个全局锁保护。这意味着任意时刻只能有一个线程访问 Map,无论是读还是写,结果就是并发退化为串行执行,性能极低。

我们的目标很明确:✅
在保证线程安全的前提下,尽可能提升并发访问能力。

3. 锁分段(Lock Striping)原理

锁分段的核心思想是:将数据结构划分为多个“段”(stripes),每个段拥有独立的锁。当线程访问某个段时,只需获取对应段的锁,而不影响其他段的操作。

这种方式实现了细粒度同步(fine-grained synchronization),允许多个线程同时操作不同段的数据,大幅提升并发性能。

实现锁分段有两种极端策略:

  • 每段一个锁:并发度最高,但内存开销大
  • 所有段共用一个锁:内存省了,但又回到了粗粒度同步的老路

为了在性能与内存之间取得平衡,Guava 提供了 Striped 工具类。它能高效管理大量锁(或信号量),支持按需分配 ReentrantLockSemaphore,其设计思想与 ConcurrentHashMap 内部的分段锁机制一脉相承,但更加通用和灵活。

4. 实战示例

下面我们通过一个性能对比实验,直观感受锁分段带来的提升。

我们将对比四种组合:

数据结构 同步方式 类名
HashMap 单锁 SingleLock
ConcurrentHashMap 单锁 SingleLock
HashMap 锁分段 StripedLock
ConcurrentHashMap 锁分段 StripedLock

实验中,多个线程并发执行 put 和 get 操作,我们观察不同策略下的吞吐量表现。

4.1. 依赖引入

使用 Guava 的 Striped 类,需添加以下 Maven 依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

4.2. 核心执行逻辑

定义抽象基类 ConcurrentAccessExperiment,统一管理并发任务的调度:

public abstract class ConcurrentAccessExperiment {

    public final Map<String, String> doWork(Map<String, String> map, int tasks, int slots) {
        CompletableFuture<?>[] requests = new CompletableFuture<?>[tasks * slots];

        for (int i = 0; i < tasks; i++) {
            requests[slots * i + 0] = CompletableFuture.supplyAsync(putSupplier(map, i));
            requests[slots * i + 1] = CompletableFuture.supplyAsync(getSupplier(map, i));
            requests[slots * i + 2] = CompletableFuture.supplyAsync(getSupplier(map, i));
            requests[slots * i + 3] = CompletableFuture.supplyAsync(getSupplier(map, i));
        }
        CompletableFuture.allOf(requests).join();

        return map;
    }

    protected abstract Supplier<?> putSupplier(Map<String, String> map, int key);
    protected abstract Supplier<?> getSupplier(Map<String, String> map, int key);
}

💡 说明:每个任务执行 1 次 put 和 3 次 get,模拟读多写少的典型场景。任务数与 CPU 核心数匹配,避免过度线程竞争。

4.3. 单锁实现(SingleLock)

使用一个 ReentrantLock 保护整个 Map:

public class SingleLock extends ConcurrentAccessExperiment {
    ReentrantLock lock;

    public SingleLock() {
        lock = new ReentrantLock();
    }

    protected Supplier<?> putSupplier(Map<String, String> map, int key) {
        return (()-> {
            lock.lock();
            try {
                return map.put("key" + key, "value" + key);
            } finally {
                lock.unlock();
            }
        });
    }

    protected Supplier<?> getSupplier(Map<String, String> map, int key) {
        return (()-> {
            lock.lock();
            try {
                return map.get("key" + key);
            } finally {
                lock.unlock();
            }
        });
    }
}

⚠️ 踩坑提醒:即使使用 ConcurrentHashMap,若外部加了单锁,其内部并发机制也完全失效,性能等同于同步 Map。

4.4. 锁分段实现(StripedLock)

使用 Guava 的 Striped 为每个“桶”分配独立锁:

public class StripedLock extends ConcurrentAccessExperiment {
    Striped<Lock> lock; // 注意泛型

    public StripedLock(int buckets) {
        lock = Striped.lock(buckets);
    }

    protected Supplier<?> putSupplier(Map<String, String> map, int key) {
        return (()-> {
            int bucket = key % lock.size(); // 计算所属桶
            Lock lock = this.lock.get(bucket);
            lock.lock();
            try {
                return map.put("key" + key, "value" + key);
            } finally {
                lock.unlock();
            }
        });
    }

    protected Supplier<?> getSupplier(Map<String, String> map, int key) {
        return (()-> {
            int bucket = key % lock.size();
            Lock lock = this.lock.get(bucket);
            lock.lock(); 
            try {
                return map.get("key" + key);
            } finally {
                lock.unlock();
            }
        });
    }
}

Striped.lock(n) 会创建 n 个锁,通过哈希或模运算将 key 映射到具体锁,实现隔离。

5. 性能测试结果

使用 JMH(Java Microbenchmark Harness) 进行压测,结果如下(单位:ops/ms,越高越好):

Benchmark                                                Mode  Cnt  Score   Error   Units
ConcurrentAccessBenchmark.singleLockConcurrentHashMap   thrpt   10  0,059 ± 0,006  ops/ms
ConcurrentAccessBenchmark.singleLockHashMap             thrpt   10  0,061 ± 0,005  ops/ms
ConcurrentAccessBenchmark.stripedLockConcurrentHashMap  thrpt   10  0,065 ± 0,009  ops/ms
ConcurrentAccessBenchmark.stripedLockHashMap            thrpt   10  0,068 ± 0,008  ops/ms

📊 结果分析:

  • 单锁模式下,HashMapConcurrentHashMap 性能几乎一致 —— 因为锁粒度已覆盖整个操作
  • 使用锁分段后,**性能提升约 10%~12%**,且 HashMap + StripedLock 组合表现最佳
  • 说明:在可控并发场景下,手动细粒度锁可优于内置并发结构的默认策略

6. 总结

锁分段是一种简单粗暴但非常有效的并发优化手段。通过降低锁的粒度,让多线程真正“并行”起来,避免不必要的串行化等待。

本文通过对比实验证明:

  • ❌ 粗粒度锁是并发性能的“隐形杀手”
  • Striped 工具类让实现锁分段变得极其简单
  • ✅ 在读多写少、访问分布均匀的场景下,锁分段收益明显

🔗 示例代码已托管至 GitHub:https://github.com/tech-tutorial/core-java-concurrency/tree/main/lock-striping

实际项目中,若发现 ConcurrentHashMap 仍存在热点竞争(如某些 key 被频繁访问),可考虑结合 Striped 进一步拆分锁,轻松完成性能调优。


原始标题:Introduction to Lock Striping