Lock Striping 개념 정리

Lock Striping은 멀티스레드 환경에서 공유 데이터에 접근할 때, 하나의 큰 락 대신 여러 개의 작은 락을 나눠 사용해 락 경합을 줄이는 기법입니다.

1. 왜 필요한가요? (Global Lock의 한계)

기존에는 Hashtable이나 SynchronizedMap처럼 데이터 구조 전체에 하나의 큰 락(Global Lock)을 거는 방식이 많이 사용되었습니다.

문제는 서로 독립적인 데이터를 다루는 작업까지 같은 락에서 대기한다는 점입니다.

예를 들어 스레드 A가 1번 데이터를 수정하는 동안, 스레드 B가 전혀 관계없는 100번 데이터를 수정하려 해도 global lock이 풀릴 때까지 기다려야 합니다. 데이터 충돌이 아니라 락 설계 때문에 병목이 생기는 것입니다.

2. Lock Striping의 해결책

핵심 아이디어는 데이터를 여러 구역(stripe)으로 나누고, 각 구역마다 독립적인 락을 두는 것입니다.

데이터 구조를 일정한 개수의 버킷이나 구역으로 쪼갭니다(Stripe). 그리고 각 구역마다 독립적인 락을 할당합니다.

  • 예를 들어, 100개의 데이터를 10개의 구역으로 나누고 10개의 락을 사용합니다.
  • 스레드 A가 1번 구역의 락을 잡고 작업하더라도, 스레드 B는 2번 구역의 락을 잡고 동시에 작업할 수 있습니다. (Lock Contention 감소)

3. Pseudo Code

키의 해시값을 이용해 어떤 stripe의 락을 사용할지 결정합니다.

class StripedMap<K, V> {
    private final Map<K, V>[] buckets;
    private final Lock[] locks;

    StripedMap(int stripeCount) {
        this.buckets = new Map[stripeCount];
        this.locks = new Lock[stripeCount];

        for (int i = 0; i < stripeCount; i++) {
            buckets[i] = new HashMap<>();
            locks[i] = new ReentrantLock();
        }
    }

    void put(K key, V value) {
        int stripe = Math.floorMod(key.hashCode(), locks.length);
        Lock lock = locks[stripe];

        lock.lock();
        try {
            buckets[stripe].put(key, value);
        } finally {
            lock.unlock();
        }
    }

    V get(K key) {
        int stripe = Math.floorMod(key.hashCode(), locks.length);
        Lock lock = locks[stripe];

        lock.lock();
        try {
            return buckets[stripe].get(key);
        } finally {
            lock.unlock();
        }
    }
}

여러 key를 한 번에 처리해야 한다면 주의가 필요합니다. 서로 다른 stripe의 락을 여러 개 잡아야 할 때는 항상 같은 순서로 락을 획득해야 deadlock 가능성을 줄일 수 있습니다.

4. 장점과 단점

4.1. 장점 (Pros)

  1. 높은 처리량 (High Throughput): 서로 다른 구역에 접근하는 스레드들이 락 경합 없이 동시에 실행되므로 성능이 크게 향상됩니다.
  2. 병목 감소: 여러 스레드가 동시에 쓰기(Write) 작업을 수행할 수 있습니다.
  3. 구현 난이도 대비 효과가 큼: 완전한 lock-free 자료구조보다 단순하면서도 global lock보다 나은 동시성을 얻을 수 있습니다.

4.2. 단점 (Cons)

  1. 전체 잠금 비용 증가: size()를 구하거나 자료구조를 확장(Resize/Rehash)할 때처럼 전체 데이터를 잠가야 하는 경우, 모든 구역의 락을 획득해야 하므로 비용이 매우 큽니다.
  2. 메모리 오버헤드: 하나의 락만 유지하던 때와 달리 여러 개의 락 객체를 메모리에 유지해야 합니다.
  3. 분포 품질에 영향받음: 해시 분포가 나쁘거나 특정 key에 요청이 몰리면 일부 stripe에만 경합이 집중될 수 있습니다.
  4. 복합 연산이 까다로움: 여러 stripe를 동시에 건드리는 연산은 락 획득 순서와 예외 처리를 신경 써야 합니다.

5. 실제 활용 사례

5.1. Java ConcurrentHashMap (Java 7 이전)

가장 대표적인 Lock Striping의 사례입니다. 기본적으로 16개의 세그먼트(Segment)로 데이터를 나누고, 16개의 락을 사용했습니다. (참고로 Java 8부터는 Striping을 넘어 버킷의 Node 단위로 락을 거는 더 세밀한 CAS + Synchronized 방식으로 진화했습니다.)

5.2. Google Guava의 Striped 클래스

명시적으로 Lock Striping을 구현할 수 있도록 도와주는 유틸리티입니다. 특정 키(Key)나 오브젝트 배열에 대해 N개의 락을 매핑하여 안전하게 동시성을 제어할 수 있게 해줍니다.

// Guava Striped 예시
Striped<Lock> stripedLocks = Striped.lock(10); // 10개의 락 구역 생성
Lock lock = stripedLocks.get("myKey"); // 키 해시 기반으로 특정 락 획득

lock.lock();
try {
    // 임계 구역 (Critical Section)
} finally {
    lock.unlock();
}

이 문서는 사용자의 요청에 의해 임시로 생성되었습니다. Written by Gemini 3.1-pro-preview via Gemini CLI Reviewed by GPT-5.5 Codex Edited by GPT-5.5 Codex

🔒 Admin 로그인