멀티스레드와 동시성

Ch09. 생산자 소비자 문제 - 스레드의 대기

webmaster 2024. 8. 24. 13:32
728x90

synchronized 대기

  • 대기 1: 락 획득 대기
    • "BLOCKED" 상태로 락 획득 대기
    • synchronized를 시작할 락이 없으면 대기
    • 다른 스레드가 synchronized를 빠져나갈 대기가 풀리며 획득 시도
  • 대기 2: wait() 대기
    • "WAITING" 상대로 대기
    • wait()를 호출했을 스레드 대기 집합에서 대기
    • 다른 스레드가 notify() 호출했을 빠져나감

synchronized 대기 - 초기 상태

  • 소비자 스레드: c1 , c2 , c3
  • 생산자 스레드: p1 , p2 , p3

실행 결과 분석 - Synchronized 대기

  • 소비자 스레드 c1 , c2 , c3 가 동시에 실행된다고 가정하자.
  • 소비자 스레드 c1 이 가장 먼저 락을 획득한다.
  • c2 , c3는 락 획득을 대기하며 "BLOCKED" 상태가 된다.

c2 , c3는 락 획득을 시도하지만, 모니터 락이 없기 때문에 락을 대기하며 "BLOCKED" 상태가 된다.

c1 나중에 락을 반납할 것이다. 그러면 c2 , c3 중에 하나가 락을 획득해야 한다. 그런데 잘 생각해 보면 락을 기다리는 c2 , c3 어딘가에서 관리가 되어야 한다. 그래야 락이 반환되었을 자바가 c2 , c3 중에 하나를 선택해서 락을 제공할 있다. 예를 들어서 List , Set , Queue 같은 자료구조에 관리가 되어야 한다. 그림에서는 c2 , c3 단순히 "BLOCKED" 상태로 변경만 되었다. 그래서 관리되는 것처럼 보이지는 않는다.

락 대기 집합이 포함된 synchronized 대기

synchronized 대기 - 락 대기 집합(초기 상태)

  • 이 그림은 이전 그림과 같은 상태를 좀 더 자세히 그린 그림이다.
  • 그림을 보면 락 대기 집합이라는 곳이 있다. 이곳은 락을 기다리는 "BLOCKED" 상태의 스레드들을 관리한다.
  • 락 대기 집합은 자바 내부에 구현되어 있기 때문에 모니터 락과 같이 개발자가 확인하기는 어렵다.
  • 여기서는 "BLOCKED" 상태의 스레드 c2 , c3 가 관리된다.
  • 언젠가 c1 이 락을 반납하면 락 대기 집합에서 관리되는 스레드 중 하나가 락을 획득한다.

대기 집합을 지금 설명하는 이유
참고로 지금까지는 스레드를 최대한 쉽고 단순하게 설명하기 위해 "BLOCKED" 상태에서 사용하는 대기 집합을 일부러 설명하지 않았다. 이제 여러분이 스레드를 어느 정도 이해했기 때문에 대기 집합의 개념을 설명해도 이해하는데 어려움은 없을 것이다.
사실 대기 집합에 대한 내용을 몰라도 괜찮다. 다만 지금 내용을 풀어서 설명하는 이유는 스레드가 모니터 락을 기다리는 상태와 Object.wait()를 통한 대기 상태를 헷갈릴 있기 때문이다. 부분을 명확히 하기 위해 풀어서 설명한다.

 

synchronized 대기 - 락 대기 집합(시간의 흐름1)
synchronized 대기 - 락 대기 집합(시간의 흐름2)

  • c1은 큐에 획득할 데이터가 없기 때문에 락을 반납하고, "WAITING "상태로 스레드 대기 집합에서 대기한다.

synchronized 대기 - 락 대기 집합(시간의 흐름3)

  • 이후에 락 대기 집합에 있는 c2가 락을 획득하고, 임계 영역을 수행한다. 큐에 획득할 데이터가 없기 때문에 락을 반납하고, "WAITING" 상태로 스레드 대기 집합에서 대기한다.
  • c3 동일한 로직을 수행한다.

synchronized 대기 - 락 대기 집합(시간의 흐름4)

  • p1 이 락을 획득하고 데이터를 저장한 다음 스레드 대기 집합에 이 사실을 알린다.

synchronized 대기 - 락 대기 집합(시간의 흐름5)

  • 스레드 대기 집합에 있는 c1 이 스레드 대기 집합을 빠져나간다.
  • 하지만 아직 끝난 것이 아니다. 락을 얻어서 락 대기 집합까지 빠져나가야 임계 영역을 수행할 수 있다.
  • c1 획득을 시도하지만 락이 없다. 따라서 대기 집합에서 관리된다.

개념상 대기 집합이 1 대기소이고, 스레드 대기 집합이 2 대기소이다. 2 대기소에 있는 스레드는 2 대기소를 빠져나온다고 끝이 아니다. 1 대기소까지 빠져나와야 임계 영역에서 로직을 수행할 있다. 비유를 하자면 임계 영역을 안전하게 지키기 위한 2 감옥인 것이다. 스레드는 2 감옥을 모두 탈출해야 임계 영역을 수행할 있다.

 

synchronized 대기 - 락 대기 집합(시간의 흐름6)

  • c1 은 락 획득을 기다리며 "BLOCKED" 상태로 락 대기 집합에서 기다린다.
  • 드디어 p1 이 락을 반납한다.

synchronized 대기 - 락 대기 집합(시간의 흐름7)

  • 락이 반납되면 락 대기 집합에 있는 스레드 중 하나가 락을 획득한다.  여기서는 c1이 락을 획득한다.
  • c1은 드디어 1차 대기소까지 탈출하고, 임계 영역을 수행한다.

 

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

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

여기서 락 대기 집합이 1차 대기소이고, 스레드 대기 집합이 2차 대기소라 생각하면 된다. 2차 대기소에 들어간 스레드는 2차, 1차 대기소를 모두 빠져나와야 임계 영역을 수행할 수 있다.

 

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

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

synchronized vs ReentrantLock 대기

synchronized 대기

synchronized  대기

  • 대기 1: 모니터 락 획득 대기
    • 자바 객체 내부의 락 대기 집합(모니터 락 대기 집합)에서 관리
    • "BLOCKED" 상태로 획득 대기
    • synchronized를 시작할 락이 없으면 대기
    • 다른 스레드가 synchronized를 빠져나갈 때 락을 획득 시도, 락을 획득하면 락 대기 집합을 빠져나감
  • 대기 2: wait() 대기
    • wait()를 호출했을 때 자바 객체 내부의 스레드 대기 집합에서 관리
    • "WAITING" 상태로 대기
    • 다른 스레드가 notify()를 호출했을 스레드 대기 집합을 빠져나감

ReentrantLock 대기

ReentrantLock  대기

  • 대기 1: ReentrantLock 락 획득 대기
    • ReentrantLock의 대기 큐에서 관리
    • "WAITING" 상태로 락 획득 대기
    • lock.lock()을 호출했을 때 락이 없으면 대기
    • 다른 스레드가 lock.unlock()을 호출했을 때 대기가 풀리며 락 획득 시도, 락을 획득하면 대기 큐를 빠져나감
  • 대기 2: await() 대기
    • condition.await()를 호출했을 때, condition 객체의 스레드 대기 공간에서 관리
    • "WAITING" 상대로 대기
    • 다른 스레드가 condition.signal()을 호출했을 때 condition 객체의 스레드 대기 공간에서 빠져나감

2단계 대기소
참고로 깨어난 스레드는 바로 실행되는 것이 아니다. synchronized와 마찬가지로 ReentrantLock 도 대기소가 2단계로 되어 있다. 2단계 대기소인 condition 객체의 스레드 대기 공간을 빠져나온다고 바로 실행되는 것이 아니다.
임계 영역 안에서는 항상 락이 있는 하나의 스레드만 실행될 수 있다. 여기서는 ReentrantLock의 락을 획득해야 "RUNNABLE" 상태가 되면서 그다음 코드를 실행할 있다. 락을 획득하지 못하면 "WAITING" 상태로 락을 획득할 ReentrantLock의 대기 큐에서 대기한다.

728x90