본문으로 건너뛰기
김신건의 로그

[Java] StampedLock

· 수정 · 📖 약 2분 · 882자/단어 #java #concurrent #lock #optimistic #read-write
StampedLock, java.util.concurrent.locks.StampedLock, 스탬프락, optimistic read

정의

java.util.concurrent.locks.StampedLock 는 JDK 1.8 도입의 새 락. 세 가지 모드 제공.

  1. Write lock (배타)
  2. Read lock (공유)
  3. Optimistic read (락 없이 시도, 변경 감지 시 retry)

stamp 라는 long 토큰으로 락 상태를 추적. acquire 시 stamp 반환, release 시 stamp 전달.

사용 상황

  • 읽기가 압도적으로 많고 쓰기는 거의 없는 자료구조
  • 짧은 읽기 연산: 두세 필드 읽는 수준, 긴 연산은 optimistic retry 비용이 누적
  • ReentrantReadWriteLock 으로도 부족할 때의 차선책
  • 재진입이 필요 없는 단순 락 시나리오

시각화: 세 가지 모드 전이

flowchart TD
    FREE["락 없음"]
    WRITING["쓰기 모드\n(배타)"]
    READING["읽기 모드\n(공유, N개 동시)"]
    OPT["옵티미스틱 읽기\n(락 없음)"]

    FREE -->|"writeLock()"| WRITING
    WRITING -->|"unlockWrite(stamp)"| FREE
    FREE -->|"readLock()"| READING
    READING -->|"unlockRead(stamp)"| FREE
    FREE -->|"tryOptimisticRead()"| OPT
    OPT -->|"validate(stamp) true"| FREE
    OPT -->|"validate(stamp) false"| READING
    WRITING -->|"tryConvertToReadLock(stamp)"| READING
    READING -->|"tryConvertToWriteLock(stamp)"| WRITING

핵심 메서드

StampedLock lock = new StampedLock();

// Write lock
long stamp = lock.writeLock();
try { ... } finally { lock.unlockWrite(stamp); }

// Read lock
long stamp = lock.readLock();
try { ... } finally { lock.unlockRead(stamp); }

// Optimistic read
long stamp = lock.tryOptimisticRead();
int value = data.get();                       // 락 없이 읽기
if (!lock.validate(stamp)) {                  // 변경 발생?
    stamp = lock.readLock();                  // 폴백: read lock
    try {
        value = data.get();
    } finally {
        lock.unlockRead(stamp);
    }
}

Optimistic Read 의 사상

writer 가 거의 없을 것이라 가정하고 락 없이 일단 읽는다. 끝나고 “내가 읽는 동안 write 가 있었나?” 를 validate(stamp) 로 검사. 없었다면 데이터 일관, 있었다면 read lock 으로 폴백.

volatile read 한 번 정도의 비용으로 끝나는 best case.

stamp 내부 구조

stamp 는 단순 long 이 아니라 버전 카운터 + 모드 비트 가 인코딩된 값.

stamp (64bit long)
┌──────────────────────────────────────────────────────────┐
│  상위 57bit: 버전 카운터 (write 발생마다 증가)             │
│  하위 7bit: 락 모드 (RUNIT=0000001, WBIT=10000000 등)     │
└──────────────────────────────────────────────────────────┘

validate(stamp) 는 현재 stamp 와 저장된 stamp 의 버전 비트가 같은지 비교. write 가 발생하면 버전이 증가하므로 mismatch 가 된다.

실전 패턴: Point 클래스

// Java 17+: StampedLock Point 패턴
class Point {
    private double x, y;
    private final StampedLock lock = new StampedLock();

    // 쓰기: 배타 락
    public void move(double dx, double dy) {
        long stamp = lock.writeLock();
        try {
            x += dx;
            y += dy;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    // 읽기: optimistic first, read lock fallback
    public double distanceFromOrigin() {
        long stamp = lock.tryOptimisticRead();
        double cx = x, cy = y;                // 락 없이 스냅샷
        if (!lock.validate(stamp)) {           // write 발생?
            stamp = lock.readLock();
            try {
                cx = x; cy = y;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return Math.sqrt(cx * cx + cy * cy);
    }
}

Point 의 두 필드를 atomic 하게 읽기 위해 락 없이 시도, 변경 발생 시 폴백.

tryConvertToWriteLock: 조건부 업그레이드

readLock 에서 조건에 따라 write lock 으로 전환할 수 있다.

// 읽고, 조건 충족 시 쓰기 (업그레이드 시도)
public void updateIfAbsent(int newValue) {
    long stamp = lock.readLock();
    try {
        if (data != 0) return;     // 이미 있음

        // read -> write 업그레이드 시도
        long ws = lock.tryConvertToWriteLock(stamp);
        if (ws != 0) {             // 성공
            stamp = ws;
            data = newValue;
        } else {
            // 업그레이드 실패 (다른 reader 있음), write lock 명시 획득
            lock.unlockRead(stamp);
            stamp = lock.writeLock();
            data = newValue;
        }
    } finally {
        lock.unlock(stamp);
    }
}

lock.unlock(stamp) 은 stamp 모드에 따라 읽기/쓰기 unlock 을 자동 선택.

제약사항

  • 재진입 불가: 같은 스레드가 write lock 을 두 번 잡으면 deadlock
  • Condition 미지원: wait/notify 같은 조건 변수 사용 불가
  • 인터럽트 처리 메서드별 다름: 모두 인터럽트에 응답하지는 않음
  • upgrading / downgrading: tryConvertToWriteLock, tryConvertToReadLock 으로 가능 (다른 stamp 발급)

언제 적절한가

읽기 압도적으로 많고, 쓰기는 거의 없으며, 짧은 읽기 인 경우.

긴 read 작업이라면 optimistic read 도중 변경 발생 가능성이 높아 폴백 비용이 누적된다.

읽기 짧음 + 쓰기 극히 드묾 -> StampedLock optimistic read
읽기 길거나 쓰기 가끔 있음 -> ReentrantReadWriteLock
재진입 / Condition 필요   -> ReentrantReadWriteLock
단순 mutex               -> synchronized 또는 ReentrantLock

ReentrantReadWriteLock vs StampedLock

| 항목 | ReentrantReadWriteLock | StampedLock | |:---|:---|:---| | 재진입 | ✓ | ✗ | | 조건 변수 | ✓ | ✗ | | optimistic read | ✗ | ✓ | | 코드 복잡도 | 단순 | 복잡 | | 성능 (read 99%) | 좋음 | 더 좋음 | | 성능 (read 50%) | 비슷 | 비슷 또는 더 나쁨 |

함정

1. 재진입 deadlock

// ❌ 같은 스레드가 두 번 write lock
long s1 = lock.writeLock();
long s2 = lock.writeLock();  // deadlock: 자기 자신이 먼저 release 해야 하는데 block 됨

WARNING

StampedLock 은 재진입을 허용하지 않는다. 재귀 함수나 콜백 체인에서 같은 lock 을 두 번 잡으면 즉시 deadlock. 재진입이 필요하면 ReentrantReadWriteLock 을 사용.

2. stamp 0 반환

tryOptimisticRead() 가 0 을 반환하는 경우: write lock 이 잡혀 있을 때. 이때 validate 는 항상 false.

long stamp = lock.tryOptimisticRead();
if (stamp == 0) {
    // write lock 잡혀 있음, optimistic 불가
    stamp = lock.readLock();
    try { ... } finally { lock.unlockRead(stamp); }
    return;
}

3. optimistic read 범위 밖에서 validate

// ❌ validate 를 읽기 완료 전에 호출
long stamp = lock.tryOptimisticRead();
if (!lock.validate(stamp)) { ... }
// validate 후에 읽으면 의미 없음: validate 사이에 write 가 또 올 수 있음
int value = data.get();

CAUTION

validate 는 읽기 완료 후 호출해야 한다. 읽기 범위 전체를 stamp 발급과 validate 사이에 두는 것이 올바른 패턴.

인터럽트 응답 메서드 정리

일부 메서드만 인터럽트에 응답한다.

메서드인터럽트 응답
writeLock()✗ (block)
writeLockInterruptibly()
readLock()✗ (block)
readLockInterruptibly()
tryOptimisticRead()N/A (락 없음)
tryWriteLock()N/A (즉시 반환)
tryReadLock()N/A (즉시 반환)

인터럽트가 필요한 시나리오에서는 *Interruptibly() 버전을 사용할 것.

CAUTION

writeLock() 은 인터럽트를 무시하고 락이 해제될 때까지 대기한다. 스레드를 종료해야 하는 shutdownNow 등의 상황에서는 반드시 writeLockInterruptibly() 를 사용.

관련 위키

이 글의 용어 (6개)
[Java] AtomicIntegerjava
정의 는 lock-free 로 원자성을 보장 하는 래퍼. 내부적으로 CAS (Compare-And-Swap) 를 사용해 락 없이 atomic 한 read-modify-write …
[Java] ConcurrentHashMapjava
정의 는 고동시성 환경에서 사용 가능한 구현. JSR-166 (Java 5) 도입, Java 8 에서 내부 구조가 크게 재작성됐다. 과 같은 인터페이스를 제공하면서 thread-…
[Java] CopyOnWriteArrayListjava
정의 는 쓰기 시 배열 전체를 복사 하는 thread-safe 구현. 읽기에는 lock 이 전혀 없고, 쓰기에는 으로 직렬화한다. (JSR-166) 의 컬렉션. 읽기 압도적 다,…
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …
[Java] ReentrantReadWriteLockjava
정의 는 읽기 락과 쓰기 락을 분리 한 ReentrantLock 의 확장. 핵심 규칙: - 여러 reader 가 동시에 read lock 획득 가능 (병렬 읽기) - writer…
[Java] volatilejava
정의 은 Java 의 키워드. 필드에 붙이면 두 가지를 보장한다. 1. 가시성 (visibility): 한 스레드의 쓰기가 다른 모든 스레드에 즉시 보인다. CPU 캐시에 머무르…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기