낙관적 및 비관적 락 개념과 구현 방식

낙관적 락과 비관적 락 정의

비관적 락(Pessimistic Locking): 데이터가 다른 스레드에 의해 수정될 가능성을 항상 고려하는 방식입니다. 이는 자원을 항상 잠금 상태로 유지하며, 다른 스레드가 해당 자원에 접근할 때까지 차단합니다. Java에서 synchronized와 ReentrantLock은 이러한 비관적 접근 방식을 따릅니다.

낙관적 락(Optimistic Locking): 데이터가 수정되지 않을 것이라고 가정하며, 읽기 시에는 락을 적용하지 않습니다. 하지만 쓰기 시에는 변경 여부를 검증합니다. Java의 StampedLock과 AtomicInteger는 이 방식을 사용합니다.

ReadWriteLock 구조

ReentrantLock은 하나의 스레드만이临界 영역 코드를 실행할 수 있도록 보장하지만, 이는 성능 저하를 초래할 수 있습니다. ReadWriteLock은 다음과 같은 특징을 제공합니다:

  • 쓰기 시점에만 락을 적용하여 병렬 읽기를 허용
  • 읽기 시점에서는 여러 스레드가 동시에 접근 가능

다음은 ReadWriteLock을 활용한 예시입니다:


public class Counter {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock rLock = rwLock.readLock();
    private final Lock wLock = rwLock.writeLock();
    private int[] countArray = new int[10];

    public void increment(int index) {
        wLock.lock();
        try {
            countArray[index]++;
        } finally {
            wLock.unlock();
        }
    }

    public int[] getValues() {
        rLock.lock();
        try {
            return Arrays.copyOf(countArray, countArray.length);
        } finally {
            rLock.unlock();
        }
    }
}

이 구조는 읽기 시 다중 스레드 처리를 최적화합니다. 그러나 동시 읽기 중 쓰기 요청이 발생하면 락 대기 시간이 증가할 수 있습니다.

StampedLock 기능

Java 8부터 도입된 StampedLock은 ReadWriteLock과 달리 읽기 중에도 쓰기 가능합니다. 이는 버전 검증을 통해 데이터 일관성을 유지합니다:


class Coordinate {
    private final StampedLock stampedLock = new StampedLock();
    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead();
        double currentX = x;
        double currentY = y;

        if (!stampedLock.validate(stamp)) {
            stamp = stampedLock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

이 구현은 버전 검증을 통해 읽기 중 쓰기가 발생했는지 확인하고, 필요 시 락 재획득을 수행합니다. 이 방식은 단일 락으로 처리할 수 없어 재진입 불가능한 특징을 가집니다.

Semaphore 활용

Semaphore는 동시성 제어를 위한 신호량 기반 락입니다. 다음은 최대 3개의 스레드만 접근을 허용하는 예시입니다:


public class ResourceController {
    private final Semaphore accessControl = new Semaphore(3);

    public String getResource() throws InterruptedException {
        accessControl.acquire();
        try {
            return UUID.randomUUID().toString();
        } finally {
            accessControl.release();
        }
    }

    public boolean tryAcquireWithTimeout() {
        return accessControl.tryAcquire(3, TimeUnit.SECONDS);
    }
}

tryAcquire() 메서드는 지정된 시간 동안 락 획득을 시도합니다. 이 방식은 리소스 제한 조건에 따라 유연하게 처리할 수 있습니다.

태그: java concurrency StampedLock Semaphore ReadWriteLock

5월 29일 18:04에 게시됨