ReentrantLock

sungs·2025년 7월 12일

자바

목록 보기
40/95

ReentrantLock

자바에서 기존 synchronized 단점을 극복하기 위해 도입하였다.
동기화에는 무한 대기, 나중에 들어온 스레드가 먼저 락을 획득하는 공정성 문제가 있었는데 ReentrantLock을 사용하면 락을 더 유연하게 사용할 수 있게 된다.

정확히는 Lock 인터페이스를 구현한 구현체다. 인터페이스에는 락을 조절할 수 있는 다향한 메서드가 있다.

참고로 여기서 쓰이는 락은 모니터 락이 아니다. Lock 인터페이스의 락이 쓰인다.

lock()

  • 락을 획득한다. 만약에 얻지 못하면 waiting 상태로 계속 기다린다. 인터럽트로도 깨울 수 없다.

lockInterruptibly()

  • 락을 획득한다. 얻지 못하면 똑같이 waiting 상태로 기다리지만 인터럽트로 꺠울 수 있다. 당연히 InterruptedException도 발생한다.

tryLock()

  • 락을 획득하면 true를 즉시 반환한다. 얻지 못한다면 바로 포기하고 false를 반환한다.

tryLock(long time, TimeUnit unit)

  • 특정 시간만큼 기다렸다가 락을 획득하면 true를 즉시 반환한다. 얻지 못한다면 바로 포기하고 false를 반환한다. 특정 시간 동안 기다리므로 스레드 상태는 Timed_waiting이다.

unlock()

  • 락을 반납한다. 스레드는 runnalbe 상태로 돌아간다. 락을 획득한 스레드가 호출해야 하며, 그렇지 않으면 IllegalMonitorStateException이 발생할 수 있다.

Codition new Codition()

  • Condition 객체를 생성한다. 이를 이용해 스레드에다가 특정 조건을 붙이거나 신호를 줄 수 있다.

예제

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {

    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock(); // ReentrantLock 인스턴스 생성

    public void increment() {
        lock.lock(); // 락 획득
        try {
            // 이 블록 내의 코드는 한 번에 하나의 스레드만 실행할 수 있습니다.
            count++;
            System.out.println(Thread.currentThread().getName() + ": Count = " + count);
        } finally {
            lock.unlock(); // 락 해제 (매우 중요! finally 블록에서 호출하여 예외 발생 시에도 락이 풀리도록 함)
        }
    }

    public int getCount() {
        // 읽기 작업이더라도 일관성 유지를 위해 락을 사용할 수 있습니다.
        // 여기서는 간단한 예시이므로 락을 사용하지 않았지만,
        // 복잡한 읽기 작업이나 쓰기 작업과 섞이는 경우에는 고려할 수 있습니다.
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockExample example = new ReentrantLockExample();

        // 5개의 스레드 생성
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment(); // 각 스레드가 1000번씩 카운터 증가
                }
            }, "Thread-" + (i + 1));
        }

        // 스레드 시작
        for (Thread thread : threads) {
            thread.start();
        }

        // 모든 스레드가 종료될 때까지 대기
        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("--- 최종 결과 ---");
        System.out.println("최종 카운트: " + example.getCount()); // 기대값: 5 * 1000 = 5000
    }
}

예제에서 볼 수 있듯이 private final ReentrantLock lock = new ReentrantLock(); 형식으로 만들 수 있다.
메서드는 간단하게 락이름.메서드()로 사용하면 된다. 예제에서 하듯이 공유 자원이 있는 임계 영역에 걸어주면 된다. 어려울 건 없다.

공정 모드

사용하면 기존 동기화의 공정성 문제를 해결할 수 있다.
private final ReentrantLock lock = new ReentrantLock(true);로 하면 공정성 모드가 된다.
이렇게 되면 가장 오래 기다린 스레드가 먼저 락을 획득하게 된다. 즉 선입선출이 보장되므로 오래 기다린 스레드가 먼저 락을 획득하지 못하는 공정성 문제가 해결된다.
다만 성능 저하가 발생된다. 아무래도 순서를 신경쓰기 때문이다.
따라서 정말로 필요한 경우에만 사용하는 게 좋다.

profile
앱 개발 공부 중

0개의 댓글