Java - Lock Condition

INHEES·2025년 1월 3일
0

금일은 Java Thread 의 생산자와 소비자 문제에 있어 앞서 포스팅 글에서의 비효율성을 해결하는 방법에 대해 알아보겠습니다.

목차

  • Lock Condition
  • 생산자 소비자 대기 공간 분리
  • 스래드의 대기

Lock Condition

앞서 포스팅에 작성한 코드에 Lock 인터페이스오 ReentrantLock 구현체를 사용해서 구현해 보겠습니다.


    private final Lock lock = new ReentrantLock();
    private final Condition producerCond = lock.newCondition();
    private final Condition consumerCond = lock.newCondition();

    private final Queue<String> queue = new ArrayDeque<>();
    private final int max;

    public BoundedQueueV5(int max) {
        this.max = max;
    }

    @Override
    public void put(String data) {
        lock.lock();
        try {
            while (queue.size() == max) {
                log("[put] 큐가 가득 참, 생산자 대기");
                try {
                    producerCond.await();
                    log("[put] 생산자 깨어남");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            queue.offer(data);
            log("[put] 생산자 데이터 저장, consumerCond.signal() 호출");
            consumerCond.signal();
        } finally {
            lock.unlock();
        }

    }

    @Override
    public String take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                log("[take] 큐에 데이터가 없음, 소비자 대기");
                try {
                    consumerCond.await();
                    log("[take] 소비자 깨어남");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            String data = queue.poll();
            log("[take] 소비자 데이터 획득, producerCond.signal() 호출");
            producerCond.signal();
            return data;
        } finally {
            lock.unlock();
        }
    }

  • condition 변수는 대기 집합을 의미합니다.
    • ReentrantLock 을 사용하는 스레드가 대기하는 스레드 대기 공간입니다.
    • ReentrantLock 은 내부에 락과, 락 획득을 대기하는 스레드를 관리하는 대기 큐가 있습니다.
  • 기존의 wait() 함수는 condition.await() 함수로 변경합니다.
  • notify() 함수는 condition.signal() 함수로 변경합니다.
  • put, take 함수 안에는 lock.lock() 함수와 try 구문의finally 안에 unlock() 함수는 필수이다.
  • put() 함수에서의 생산자와 소비자의 condition 함수의 배치순서를 유의해야한다.

Condition.signal() 함수는 Object.notify() gka수와는 다르게 임의의 스레드를 깨우는 것이 아니라 fifo 순서로 깨우게 된다. 이 부분은 자바 버전과 구현에 따라 달라질 수 있지만 보통 Queue 구조를 사용하기 때문에 fifo 순서를 유지합니다.

또한 syncronized 블록 내에서 모니터 락을 가지는 스레드 호출이 아니라 ReentrantLock 을 가지고 있는 스레드가 호출해야 합니다.


스레드의 대기

사실은 BLOCKED 상태의 스레드도 자바 내부에서 따로 관리된다.

c2,c3 가 단순히 BLOCKED 상태돌 변경만 된것이 아니라 내부의 자료구조에 의해 관리 되어 지는 것이다.

스레드 대기 집합에 있는 c1 이 스레드 대기 집합을 빠져 나오게 되면 락을 얻어서 락 대기 집합까지 빠져나가야 임계 영역을 수행할 수 있는 것이다.

만약 락을 획득할 수 없으면 락 대기 집합에서 관리 되는 것이다.

정리

자바의 모든 객체 인스턴스는 멀티스레드와 임계 영역을 다루기 위해 내부에 3가지 기본 요소를 가지게 됩니다.

  • 모니터 락
  • 락 대기집합(모니터 락 대기 집합)
  • 스레드 대기 집합

이 3가지 요소는 서로 맞물려 돌아간다.

  • synchronized 를 사용한 임계 영역에 들어가려면 모니터 락이 필요하다.
  • 모니터 락이 없으면 락 대기 집합에 들어가서 BLOCKED 상태로 락을 기다린다.
  • 모니터 락을 반납하면 락 대기 잡합에 있는 스레드 중 하나가 락을 획득하고 BLOCKED -> RUNNABLE 상태가
    된다.
  • wait() 를 호출해서 스레드 대기 집합에 들어가기 위해서는 모니터 락이 필요하다.
  • 스레드 대기 집합에 들어가면 모니터 락을 반납한다.
  • 스레드가 notify() 를 호출하면 스레드 대기 집합에 있는 스레드 중 하나가 스레드 대기 집합을 빠져나온다. 그
    리고 모니터 락 획득을 시도한다.
    • 모니터 락을 획득하면 임계 영역을 수행한다.
    • 모니터 락을 획득하지 못하면 락 대기 집합에 들어가서 BLOCKED 상태로 락을 기다린다

synchronized vs ReentrantLock 대기

모니터 락 획득 대기

  • 자바 객체 내부의 락 대기 집합에서 관리
  • BLOCKED 상태로 락 획득 대기
  • syncronized 를 시작할 때 락이 없으면 대기

wait() 대기

  • wait() 를 호출 했을 때 자바 객체 내부의 스레드 대기 집합에서 관리
  • WAITING 상태로 대기
  • 다른 스레드가 notify()를 호출 했을 대 스레드 대기 집합을 빠져나감

ReentrantLock 락 획득 대기

  • ReentrantLock 의 대기 큐에서 관리
  • WAITING 상태로 락 획득 대기
  • lock.lock() 을 호출 했을 때 락이 없으면 대기

await() 대기

  • condition.await() 를 호출 했을 때 , condition 객체의 스레드 대기 공간에서 관리
  • WAITING 상태로 대기
  • 다른 스레드가 signal() 을 호출 햇을 때 condition 객체의 스레드 대기 공간에서 빠져 나감

사실 개념은 같다고 봐야된다. 단 락 대기 집합에서의 상태값만 다르다고 보면 된다. syncronized 의 BLOCKED 상태는 깨울 수 있는 방법이 없다.

반대로 ReentrantLock 은 WAITING 상태 이므로 중간에 깨울 수 있으므로 유연한 설계가 가능하다.

profile
이유를 찾아보자

0개의 댓글