Introduction to Lock Striping – 锁定条纹的介绍

最后修改: 2020年 3月 20日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this tutorial, we’re going to learn how to achieve fine-grained synchronization, also known as Lock Striping, a pattern for handling concurrent access to data structures while keeping up a good performance.

在本教程中,我们将学习如何实现细粒度的同步,也被称为 “锁条”,这是一种处理数据结构并发访问的模式,同时保持良好的性能。

2. The Problem

2.问题

HashMap is not a thread-safe data structure due to its non-synchronized nature. That means commands from a multi-threaded environment might result in data inconsistency.

HashMap由于其非同步性质,不是一个线程安全的数据结构。这意味着来自多线程环境的命令可能会导致数据的不一致性。

To overcome that issue, we can either convert the original map with Collections#synchronizedMap method or use the HashTable data structure. Both will return a thread-safe implementation of the Map interface, but they come at the cost of performance.

为了克服这个问题,我们可以用Collections#synchronizedMap方法转换原始地图,或者使用HashTable数据结构。这两种方法都将返回Map接口的线程安全实现,但它们的代价是性能。

The approach of defining exclusive access over data structures with a single lock object is called coarse-grained synchronization.

用单个锁对象对数据结构定义排他性访问的方法被称为粗粒度同步

In a coarse-grained synchronization implementation, every access to the object must be made at a time by one thread. We end up having sequential accesses.

在一个粗粒度的同步实现中,对对象的每一次访问都必须由一个线程来完成。我们最终会有顺序的访问。

Our goal is to allow concurrent threads to work on the data structure while ensuring thread-safety.

我们的目标是允许并发的线程在数据结构上工作,同时确保线程安全。

3. Lock Striping

3.锁定条纹

To reach our goal, we’ll use the Lock Striping pattern. Lock striping is a technique where the locking occurs on several buckets or stripes, meaning that accessing a bucket only locks that bucket and not the entire data structure.

为了达到我们的目标,我们将使用锁条化模式。锁定条带化是一种技术,锁定发生在几个桶或条带上,这意味着访问一个桶只锁定该桶,而不是整个数据结构。

There are a couple of ways to do this:

有几种方法可以做到这一点。

  • First, we could use a lock per task, thus maximizing concurrency between tasks – this has a higher memory footprint, though
  • Or, we could use a single lock for every task, which makes use of less memory but also compromises performance in concurrency

To help us manage this performance-memory tradeoff, Guava ships with a class called Striped. It’s similar to logic found in ConcurrentHashMap, but the Striped class goes even further by reducing the synchronization of distinct tasks using semaphores or reentrant locks.

为了帮助我们管理这种性能与内存的权衡,Guava提供了一个名为Striped的类。它与ConcurrentHashMap中的逻辑相似,但是Striped类通过使用semaphores或reentrant locks减少不同任务的同步而更进一步。

4. A Quick Example

4.一个快速的例子

Let’s do a quick example to help us understand the benefits of this pattern.

让我们做一个简单的例子来帮助我们理解这种模式的好处。

We’ll compare HashMap vs. ConcurrentHashMap and a single lock vs. a striped lock resulting in four experiments.

我们将比较HashMapConcurrentHashMap以及单锁与带状锁,从而进行四个实验。

For each experiment, we’ll perform concurrent reads and writes on the underlying Map. What will vary is how we access each bucket.

对于每个实验,我们将在底层Map上执行并发的读和写。不同的是我们如何访问每个桶。

And for that, we’ll create two classes – SingleLock and StripedLock. These are concrete implementations of an abstract class ConcurrentAccessExperiment that does the work.

为此,我们将创建两个类–SingleLockStripedLock。这些都是抽象类ConcurrentAccessExperiment的具体实现,该类负责相关工作。

4.1. Dependencies

4.1. 依赖性

Since we’re going to use Guava’s Striped class, we’ll add the guava dependency:

由于我们要使用Guava的Striped类,我们将添加guava依赖。

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

4.2. Main Process

4.2.主要过程

Our ConcurrentAccessExperiment class implements the behavior previously described:

我们的ConcurrentAccessExperiment类实现了之前描述的行为。

public abstract class ConcurrentAccessExperiment {

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

        for (int i = 0; i < threads; 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);
}

It’s important to note that, as our test is CPU-bound, we have limited the number of buckets to some multiple of the available processors.

值得注意的是,由于我们的测试是受CPU限制的,我们将桶的数量限制在可用处理器的某个倍数。

4.3. Concurrent Access with ReentrantLock

4.3.使用ReentrantLock的并发访问

Now we’ll implement the methods for our asynchronous tasks.

现在我们将实现我们的异步任务的方法。

Our SingleLock class defines a single lock for the entire data structure using a ReentrantLock:

我们的SingleLock类使用ReentrantLock为整个数据结构定义了一个单锁。

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();
            }
        });
    }
}

4.4. Concurrent Access with Striped

4.4.用Striped进行并发访问

Then, the StripedLock class defines a striped lock for each bucket:

然后,StripedLock类为每个桶定义了一个带状锁。

public class StripedLock extends ConcurrentAccessExperiment {
    Striped lock;

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

    protected Supplier<?> putSupplier(Map<String,String> map, int key) {
        return (()-> {
            int bucket = key % stripedLock.size();
            Lock lock = stripedLock.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 % stripedLock.size();
            Lock lock = stripedLock.get(bucket);
            lock.lock(); 
            try {
                return map.get("key" + key);
            } finally {
                lock.unlock();
            }
        });
    }
}

So, which strategy performs better?

那么,哪种策略表现得更好?

5. Results

5.结果

Let’s use JMH (the Java Microbenchmark Harness) to find out. The benchmarks can be found through the source code link at the end of the tutorial.

让我们使用JMH(Java Microbenchmark Harness)来了解一下。这些基准可以通过本教程末尾的源代码链接找到。

Running our benchmark, we’re able to see something similar to the following (note that higher throughput is better):

运行我们的基准,我们能够看到与下面类似的情况(注意,吞吐量越高越好)。

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

6. Conclusions

6.结论

In this tutorial, we explored different ways of how we can achieve better performance using Lock Striping in Map-like structures. We created a benchmark to compare the results with several implementations.

在本教程中,我们探讨了如何在Map类结构中使用锁条化实现更好的性能的不同方法。我们创建了一个基准来比较几个实现的结果。

From our benchmark results, we can understand how different concurrent strategies could significantly affect the overall process. Striped Lock pattern grants quite an improvement as it scores ~10% extra with both HashMap and ConcurrentHashMap.

从我们的基准结果中,我们可以理解不同的并发策略是如何显著影响整个过程的。带状锁模式带来了相当大的改进,因为它比HashMapConcurrentHashMap都多了10%的分数。

As usual, the source code for this tutorial is available over on GitHub.

像往常一样,本教程的源代码可以在GitHub上获得。