JAVA/비동기 프로그래밍

[Java] Lock, ReentrantLock, ReadWriteLock, ReentrantReadWriteLock

꾸준함. 2024. 3. 9. 00:02

개요

앞선 게시물을 읽고 오시는 것을 추천드립니다!

https://jaimemin.tistory.com/2409

 

[Java] synchronized, wait() & notify(), volatile, Deadlock

개요 앞선 게시물을 읽고 오시는 것을 추천드립니다! https://jaimemin.tistory.com/2392 [Java] 동기화 개념 1. 싱글 쓰레드 vs 멀티 쓰레드 프로세스는 오직 한 개의 쓰레드로만 구성하는 싱글 쓰레드 프로

jaimemin.tistory.com

 

synchronized vs Lock 구현

  • Lock 구현은 synchronized 구문과 마찬가지로 상호 배제와 가시성 기능을 가진 동기화 기법
  • Lock 구현은 synchronized보다 더 확장된 락 작업 제공
    • tryLock(): 락 획득 시 블록 되지 않는 비차단 시도
    • lockInterruptibly(): 인터럽트가 가능한 방식으로 락을 획득 시도
    • tryLock(long, TimeUnit): 시간 제한 내 락 획득 시도

 

  • synchronized 사용은 락 획득과 락 해제가 블록 구조화된 방식으로 발생하도록 강제
    • 여러 락을 획득하면 반드시 반대 순서로 해제해야 함
    • 모든 락은 동일한 문장 블록 범위에서 획득하고 해제되어야 함
    • synchornized는 블록을 벗어나면 락 해제가 자동적으로 이루어짐
    • synchronized 구문은 락의 획득과 해제가 내장되어 있어 암묵적인 락

 

 

 

  • Lock 구현은 보다 유연한 방식으로 작업할 수 있도록 지원
    • 여러 락을 획득하더라도 락 해제 순서를 마음대로 정할 수 있음
    • 특정 락 A의 임계 영역은 락 A가 해제되는 순간까지
    • Lock 구현은 명시적으로 락 해제 필요
    • Lock은 락의 획득과 해제를 직접 명시해서 명시적인 락

 

 

ReentrantLock

ReentrantLock은 Java에서 제공하는 고급 동기화 기능 중 하나로, synchronized 키워드보다 더 유연하고 강력한 기능을 제공합니다.

ReentrantLock은 Java의 java.util.concurrent.locks 패키지에 속하며, 재진입(Reentrant)이 가능한 특징을 가지고 있습니다.

재진입은 동일한 쓰레드가 이미 보유한 락을 다시 획득할 수 있는 능력을 의미합니다.

 

ReentrantLock을 기본 생성자를 사용하여 생성할 경우 비공정 락이 생성되며, 생성자에 매개변수로 true를 전달할 경우 공정성이 적용됩니다.

  • 공정성이 설정되었을 경우 락 획득을 위해 오래 대기한 쓰레드에 우선순위 부여

 

ReentrantLock이 제공하는 주요 메서드는 아래와 같습니다.

  • void lock()
  • void lockInterruptibly() throws InterruptedException
  • boolean tryLock()
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException
  • void unlock()
  • Condition newCondition()

 

1. void lock()

 

  • 락이 다른 쓰레드에 의해 보유되고 있지 않다면 락을 즉시 획득하고 락 카운트를 1로 설정
  • ReentrantLock은 재진입이 가능하기 때문에 현재 쓰레드가 이미 해당 락을 획득했다면 카운트가 1 증가하고 메서드는 즉시 반환됨
  • 다른 쓰레드가 락을 보유하고 있을 경우 현재 쓰레드는 락을 획득할 때까지 대기
    • 이후 락 획득 시 카운트가 1로 설정

 

2. void lockInterruptibly() throws InterruptedException

 

  • 현재 쓰레드가 인터럽트 되지 않는 한 락 획득, 다른 쓰레드에 의해 보유되지 않는다면 락을 즉시 획득하고 락 카운트를 1로 설정
  • 현재 쓰레드가 이미 해당 락을 보유하고 있다면 카운트가 1 증가하고 메서드는 즉시 반환됨
  • 다른 쓰레드가 락을 보유하고 있을 경우 현재 쓰레드는 락을 획득할 때까지 대기
  • 현재 쓰레드가 해당 메서드에 진입했고 아래 상태 중 하나일 때 InterruptedException이 발생하며 인터럽트 상태 초기화
    • 인터럽트 상태가 설정되어 있는 경우
    • 락을 획득하는 도중 인터럽트가 발생하는 경우

 

  • 락을 정상적으로 혹은 재진입으로 획득하는 것보다 인터럽트를 우선적으로 처리

 

3. boolean tryLock()

 

  • 락을 호출하는 시점에 다른 쓰레드에 의해 보유되지 않을 때만 락을 회득하고 락 카운트를 1로 설정한 뒤 true를 반환
  • 락이 공정성을 가지도록 설정되었더라도 현재 다른 쓰레드가 락을 기다리는지 여부와 관계없이 락이 사용 가능한 경우 즉시 락 획득
  • 현재 쓰레드가 해당 락을 보유하고 있을 경우 카운트가 1 증가하고 true 반환
  • 다른 쓰레드가 락을 보유하고 있을 경우 해당 메서드는 즉시 false 반환
  • 해당 메서드는 락을 획득하지 못하더라도 쓰레드가 대기하거나 차단되지 않음

 

4. boolean tryLock(long time, TimeUnit unit) throws InterruptedException

 

  • 설정한 대기 시간 내 다른 쓰레드에 의해 보유되지 않을 경우 락을 획득하고 락 카운트를 1로 설정한 뒤 true 반환
  • 3번 tryLock()과 달리 락이 공정성을 가지도록 설정되어 있을 경우 락이 사용 가능한 경우에는 다른 쓰레드가 락을 기다리고 있는지 여부와 상관없이 락 획득 X
  • 현재 쓰레드가 이미 해당 락을 보유하고 있다면 카운트가 1 증가하고 메서드는 true를 반환
    • 락이 다른 쓰레드에 의해 보유되어 있다면 락을 획득할 때까지 대기

 

  • 현재 쓰레드가 해당 메서드를 호출했고 아래 상태 중 하나일 때 InterruptedException이 발생되고 인터럽트 상태 초기화
    • 인터럽트 상태가 설정되어 있는 경우
    • 락을 획득하는 도중 인터럽트가 발생하는 경우

 

  • 지정된 대기 시간 결과 시 false 반환
  • 대기 시간이 0 이하일 경우 메서드는 전혀 대기하지 않음
  • 락을 정상적으로 혹은 재진입으로 획득하는 것보다 인터럽트를 우선적으로 처리

 

5. void unlock()

 

  • 락 해제 시도하는 메서드
  • 락을 해제하려면 동일한 쓰레드에서 lock() 메서드가 호출된 횟수와 동일한 횟수로 호출 필요
    • unlock() 메서드가 호출될 때마다 락 카운트가 감소되며 락 카운트가 0이 되면 락 해제

 

  • 현재 쓰레드가 해당 락의 소유자가 아닌 경우 IllegalMonitorStateException 발생

 

6. Condition newCondition()

 

  • Lock 인스턴스와 함께 사용하기 위한 Condition 인스턴스 반환
  • Condition 인스턴스는 Object 모니터 메서드인 wait(), notify(), notfiyAll()과 동일한 기능 제공
    • 대기 또는 신호 메서드가 호출될 때 락이 없을 경우 IllegalMonitorStateException 발생
    • 관련 메서드는 반드시 락 획득한 뒤 임계 영역 내에서 호출

 

  • await() 메서드 호출 시 락 해제
  • signal() 혹은 signallAll() 메서드 호출 시 락 획득
  • 쓰레드가 대기하는 동안 인터럽트가 발생하면 대기 종료되고 InterruptedException 발생, 인터럽트 상태 초기화
  • 대기 중인 쓰레드는 큐처럼 FIFO 순서로 신호를 받음
    • 신호에 의해 await 메서드에서 반환되는 쓰레드의 락 재획득 순서는 기본적으로 초기에 락을 획득하는 쓰레드와 동일
    • 공정성이 설정되어 있을 가장 오래 기다린 쓰레드에 우선권 부여
    • 비공정일 경우 대기 중인 쓰레드들끼리 경쟁 통해 락 획득

 

ReentrantLock vs synchronized

 

1. ReentrantLock 사용하는 케이스

  • 앞서 설명한 API를 통해 락을 획득해야 하는 경우
  • 락의 획득과 해제가 단일 블록을 벗어나는 경우 (명시적 락)

 

2. synchronized 사용하는 케이스

  • ReentrantLock의 추가 기능이 필요하지 않을 경우 synchronized 사용
    • ReentrantLock 대비 성능상 크게 차이가 나지 않음
    • 락 해제 불필요하여 편리하며 많은 개발자들에게 익숙한 코드

 

ReentrantLock에서 제공하는 모니터링 관련 메서드

 

  • int getHoldCount()
  • boolean isHeldByCurrentThread()
  • boolean hasQueuedThreads()
  • int getQueueLength()
  • boolean hasWaiters(Condition condition)
  • int getWaitQueueLength(Condition condition)
  • Collection<Thread> getWaitingThreads(Condition condition)

 

1. int getHoldCount()

  • 현재 쓰레드의락 카운트 반환
  • 락을 보유하지 않은 경우 0 반환

 

2. boolean isHeldByCurrentThread()

  • 현재 쓰레드가 해당 락을 보유하고 있는지 확인
  • 주로 디버깅이나 테스트에 사용되는 메서드

 

3. boolean hasQueuedThreads()

  • 쓰레드가 해당 락을 획득하기 위해 대기 중인지 여부 조회
  • 언제든지 대기를 취소할 수 있으므로 true를 반환한다고 해서 조회한 쓰레드가 해당 락을 획득한다고 보장 X
  • 모니터링 목적으로 사용

 

4. int getQueueLength()

  • 해당 락을 획득하기 위해 대기 중인 쓰레드 수의 추정치 반환
  • 내부 데이터 구조를 탐색하는 동안 쓰레드 수가 동적으로 변경될 수 있기 때문에 추정치 (실제 값과 상이할 수 있음)
  • 모니터링 목적으로 사용

 

5. boolean hasWaiters(Condition condition)

  • 해당 락과 관련된 지정된 Condition에 대기 중인 쓰레드가 존재하는지 확인
    • Condition은 모니터의 조건 변수라고 생각해도 무방

 

  • 타임아웃과 인터럽트가 언제든 발생할 수 있기 때문에 true를 반환한다고 해서 미래의 신호가 어떤 대기 중인 쓰레드를 깨울 것임을 보장 X
  • 모니터링 목적으로 사용

 

6. int getWaitQueueLength(Condition condition)

  • 해당 락과 관련된 지정된 Condition에 대기 중인 쓰레드의 수에 대한 추정치 반환
  • 타임아웃과 인터럽트가 언제든 발생할 수 있기 때문에 추정치는 실제 대기 중인 쓰레드 수에 대한 상한선 역할
  • 모니터링 목적으로 사용

 

7. Collection<Thread> getWaitingThreads(Condition condition)

  • 해당 락과 관련된 지정된 Condition에 대기 중인 쓰레드를 포함하는 컬렉션 반환
  • 실제 쓰레드 집합이 컬렉션을 구성하는 동안 동적으로 변경될 수 있으므로 반환된 컬렉션은 최선의 추정치에 불과
  • 모니터링 목적으로 사용

 

ReadWriteLock

  • 읽기 작업과 쓰기 작업을 위해 연관된 두 개의 락인 읽기 락과 쓰기 락을 유지하는 인터페이스
  • 데이터를 읽는 작업만 실행되는 영역은 여러 쓰레드가 동싱에 접근해도 동시성 문제 발생 X
  • 따라서 읽기 작업이 많고 쓰기 작업이 적은 영역을 효율적으로 처리하기 위해 다수의 읽기와 하나의 쓰기를 읽기 락과 쓰기 락으로 구분해서 락을 운용

 

ReadWriteLock 특징

 

1. 성능 개선

  • 읽기 락과 쓰기 락의 조합은 mutex를 사용하는 것보다 데이터에 대한 동시 접근을 허용하므로 동시성이 높아짐
  • 특히 읽기 작업이 많을 경우 효과적
  • 읽기 락의 경우 여러 쓰레드가 동시에 데이터를 읽을 수 있고 쓰기 락의 경우 하나의 쓰레드만 데이터 수정 가능

 

2. 메모리 동기화

  • 읽기 락 작업이 타 읽기 락 작업과 상호 작용하는 것이 아니기 때문에 쓰레드 간 동시에 읽기 작업을 하더라도 메모리의 가시성에 아무런 문제없음
  • 반면 쓰기 락 작업은 읽기 작업 및 다른 쓰기 작업과의 메모리 동기화를 보장해야 함
    • 쓰레드가 쓰기 락을 해제하고 다른 쓰레드가 읽기 락을 얻었을 때 이전 쓰기 작업으로 인해 업데이트된 값을 볼 수 있어야 함

 

3. 사용 기준

  • 수정은 드물게 일어나고 검색은 빈번히 발생한다면 ReadWriteLock의 사용에 적합한 이상적인 후보
  • 수정이 빈번해지면 데이터가 대부분 배타적으로 작동하기 때문에 이런 경우 ReadWriteLock보다는 일반 Lock 사용 권장
  • 읽기 작업 시간이 긴 경우 여러 쓰레드들이 경합 없이 모두 읽을 수 있는 장점이 존재하지만
    • 읽기 작업 시간이 너무 짧은 경우 ReadWriteLock 구현의 오버헤드가 증가하기 때문에 효율성이 떨어짐
    • 여기서 오버헤드란 읽기 작업과 쓰기 작업의 상태를 지속 확인하는 과정
    • 해당 과정을 구현하는 알고리즘은 mutex보다 알고리즘이 복잡함

 

ReentrantReadWriteLock

 

1. ReentrantReadWriteLock.ReadLock

 

  • 여러 읽기 쓰레드가 동시에 읽기 락을 얻을 수 있으며 읽기 락을 보유하는 동안에도 다른 읽기 쓰레드들이 읽기 락을 획득할 수 있음
  • 여러 쓰레드가 상호 배제 없이 동시에 데이터를 읽어 동시성이 증가한다는 것이 장점
  • 읽기 락이 보유되는 동안 쓰기 락을 얻을 수 없음
  • 쓰기 락을 획득하기 위해 대기하는 중에도 계속 읽기 락을 요청할 경우 쓰기락을 요청한 쓰레드가 starvation 문제가 발생할 수 있으므로 쓰기 락을 요청한 상태에서는 더 이상 쓰레드가 읽기 접근할 수 없음
  • unlock() 메서드 호출될 때 만약 현재 읽기 락의 수가 0일 경우 해당 락은 쓰기 락 획득 시도를 위해 사용 가능해짐
  • ReadLock은 Condition을 지원하지 않기 때문에 newCondition() 메서드 호출 시 UnsupportedException 발생

 

2. ReentrantReadWriteLock.WriteLock

 

  • 쓰기 락은 읽기 락과 달리 상호 배타적이며 한 번에 하나의 쓰레드만 쓰기 락을 보유할 수 있고 쓰기 락을 보유하는 동안에는 다른 어떤 쓰레드도 읽기 락이나 쓰기 락을 얻을 수 없음 (mutex)
  • 쓰기 락이 보유되는 동안 데이터를 수정하는 작업을 수행하며 해당 작업이 완료될 때까지 다른 쓰레드가 해당 락을 얻지 못함
  • WriteLock은 ReadLock과 달리 Condition을 지원
    • 읽기 락은 쓰기 락과 독립적으로 소유되기 때문에 영향을 주지 않지만 현재 쓰레드가 읽기 락도 획득한 상태에서 newCondition()을 호출하는 것은 사실상 항상 오류
    • 대기를 해제할 수 있는 다른 쓰레드도 쓰기 락을 획득하지 못할 수 있기 때문
    • 조건 변수는 주로 특정 조건이 만족되기를 기다리는 스레드들을 관리하기 위해 사용
    • 쓰기 락을 획득한 쓰레드만이 해당 자원을 변경하고 조건 변수를 조작할 수 있어야 함
    • 여러 쓰레드가 동시에 쓰기 락을 획득하는 것을 허용하면, 여러 쓰레드가 조건 변수를 동시에 조작할 수 있으므로 제어가 어려워짐

 

 

 

 

코드 부연 설명

  • 쓰기 락은 상호 배타적이기 때문에 쓰기 락을 보유하는 동안에는 다른 어떤 쓰레드도 읽기/쓰기 락 얻을 수 없음
  • 반면, 읽기 락의 경우 동시 접근을 허용하기 때문에 동일한 시간에 읽기 쓰레드 1과 읽기 쓰레드 2가 데이터를 읽은 것을 확인 가능

 

ReentrantLock 공정성 정책

ReentrantLock은 두 종류의 락 공정성 설정을 지원합니다.

  • 불공정성 (디폴트)
  • 공정성

 

1. 불공정성

 

  • ReentrantLock lock = new ReentrantLock();
  • 불공정한 락으로 생성된 경우 경쟁 상황에서 읽기 및 쓰기 락에 대한 진입 순서는 정해지지 않음
  • 하나 이상의 읽기 또는 쓰기 쓰레드를 무기한 연기할 수 있으나 일반적으로 공정한 락보다 더 높은 처리량을 가짐
    • 간혹 드물게 특정 쓰레드가 락 획득 경쟁에서 계속 져 기아 현상이 발생할 수 있지만
    • 락을 사용하고자 하는 쓰레드가 있을 때 바로 획득하게 하는 것이 대기 중인 쓰레드를 찾아 락을 획득하도록 처리하는 시간보다 더 빠르기 때문에 대부분의 경우 공정하게 처리해서 얻는 장점보다 불공정하게 처리해서 얻는 성능상 이점이 더 큼

 

  • ReentrantLock.tryLock() 메서드는 불공정성을 따르고 대기 중인 쓰레드와 관계없이 락을 즉시 획득

 

2. 공정성

 

  • ReentrantLock lock = new ReentrantLock(true);
  • 공정한 락으로 생성된 경우 쓰레드는 도착 순서 정책을 사용하여 진입
    • 락이 해제될 때 가장 오래 기다린 단일 쓰레드가 쓰기 락을 할당받음
    • 혹은 모든 대기하는 쓰기 쓰레드보다 더 오래 기다린 읽기 쓰레드 그룹이 있을 경우 해당 그룹이 읽기 락을 할당받음

 

  • 재진입이 아닌 케이스에서 공정한 읽기 락을 획득하려는 쓰레드는  쓰기 락이 보유 중이거나 대기 중인 쓰기 쓰레드가 있는 경우 차단됨
    • 가장 오래 대기 중인 쓰기 쓰레드가 쓰기 락을 획득하고 해체한 후에 읽기 락을 획득
    • 대기 중인 쓰기 쓰레드가 대기를 포기한 상태에서 쓰기 락이 해제되어 읽기 락이 가능한 상태가 될 경우 읽기 쓰레드들이 읽기 락을 할당받음

 

  • 재진입이 아닌 케이스에서 공정한 쓰기 락을 획득하려는 쓰레드는 읽기 락과 쓰기 락 모두 대기하는 쓰레드가 없을 경우 락을 획득하고 그 외에는 차단됨
  • 공정성 락은 starvation 상태를 방지해야 하는 상황이 꼭 필요한 경우 최선책이지만 trade-off로 성능 저하 발생
  • ReentrantLock.tryLock(timeout, TimeUnit)은 공정성을 따름

 

 

코드 부연 설명

  • 성능적인 측면에서는 공정성 락이 불공정석 락에 비해 열위인 것을 확인 가능

 

ReentrantReadWriteLock 재진입성

  • ReentrantReadWriteLock은 ReentrantLock과 같이 읽기 및 쓰기 락을 다시 획득할 수 있도록 재진입을 허용
    • 쓰기 락을 보유하고 있는 쓰레드가 모든 쓰기 락을 해제하기 전까지는 재진입이 아닌 읽기 쓰레드 허용 X

 

  • 쓰기 쓰레드는 읽기 락을 획득할 수 있지만 읽기 쓰레드가 쓰기 락을 획득하려고 하면 실패
    • 락 다운그레이드는 되지만 락 업그레이드는 안됨

 

  • 쓰기 락을 보유한 쓰레드가 읽기 락 아래에서 읽기를 수행하는 메서드 혹은 콜백 호출 시 재진입 특성이 용이함

 

1. 락 다운그레이드

 

  • 재진입성은 쓰기 락에서 읽기 락으로 다운그레이드 가능
  • 이를 위해 쓰기 락을 획득한 후 읽기 락을 회득하고 마지막으로 쓰기 락을 해제

 

락 다운그레이드의 장점:

  • 쓰기 쓰레드가 데이터를 업데이트한 후에도 읽기 락을 유지하면 다른 쓰레드가 데이터를 읽는 것을 막을 수 있음
  • 다운그레이드를 사용하면 쓰기 쓰레드가 데이터를 업데이트한 후 잠시 동안 읽기 락을 유지하여 성능을 향상시킬 수 있음

 

락 다운그레이드 주의 사항:

  • 다운그레이드는 쓰기 쓰레드가 데이터를 업데이트한 후 잠시 동안 읽기 락을 유지하는 경우에만 유용
  • 쓰기 쓰레드가 오랫동안 읽기 락을 유지하면 다른 쓰레드가 데이터를 읽는 것을 막아 성능 저하를 초래할 수 있음

 

2. 락 업그레이드

 

  • 읽기 락에서 쓰기 락으로 업그레이드하는 것은 불가능
  • 읽기 락은 여러 쓰레드가 동시에 보유할 수 있기 때문에 업그레이드 불가

 

 

코드 부연 설명

  • 1개의 쓰기 쓰레드와 5개의 읽기 쓰레드가 생성
  • 쓰기 쓰레드는 먼저 쓰기 락을 획득하고 데이터를 업데이트
  • 쓰기 쓰레드는 쓰기 락을 읽기 락으로 다운그레이드
  • 읽기 쓰레드는 읽기 락을 획득하고 데이터를 읽음

 

Condition

  • Condition은 조건 변수 또는 조건 큐로 알려진 객체로써 Lock과 결합하여 객체 당 여러 개의 Wait Queue를 가지는 효과를 제공
  • Lock이 synchronized 메서드와 문장의 사용을 대체하는 것처럼 Condition은 Object 모니터 메서드인 wait(), notify(), notifyAll()의 사용을 대체하여 Lock에 바인딩
  • Condition은 한 쓰레드가 다른 쓰레드로부터 어떤 상태 조건이 참이 될 수 있다는 통지를 받을 때까지 실행을 중단하도록 하는 수단을 제공
  • Condition의 가장 중요한 특성은 락을 원자적으로 해제하고 현재 쓰레드를 중단하는 것
    • Object.wait() 메서드와 동일하게 동작

 

  • 모니터 조건변수보다 유연하여 동일한 락 내 Condition 객체 여러 개 생성 가능
    • 모니터: 하나의 조건 변수만 사용할 수 있으며, 조건 표현이 제한적
    • Condition: 여러 개의 Condition 객체를 생성하여 각각 다른 조건을 표현 가능

 

  • 모니터 메서드도 synchronized 블록 안에서 호출해야 하는 것처럼 Condition 메서드도 락을 확보한 상태에서만 호출해야 함

 

 

1. Object의 모니터 메서드에 대응되는 Lock 메서드

 

  Monitor Lock
임계 영역 설정 synchronized block lock.lock()
lock.unlock()
쓰레드 중단 object.wait() condition.await()
대기하는 임의의 쓰레드 깨움 object.notify() condition.signal()
대기하는 모든 쓰레드들을 꺠움 object.notifyAll() condition.signalAll()

 

2. Condition에서 제공하는 메서드

 

Condition에서 제공하는 메서드는 다음과 같습니다.

  • void await() throws InterruptedException
  • void awaitUninterruptibly()
  • long awaitNanos(long nanosTimeout) throws InterruptedException
  • boolean await(long time, TimeUnit unit) throws InterruptedException
  • boolen awaitUntil(Date deadline) throws InterruptedException
  • void signal()
  • void signalAll()

 

현재 쓰레드가 await 관련 메서드 실행 시 다음 네 가지 중 하나가 발생할 때까지 대기하게 되며 해당 Condition과 관련된 락은 원자적으로 해제됩니다.

  • 다른 쓰레드가 해당 Condition에 대해 signal 메서드를 호출하고 현재 쓰레드가 꺠어날 쓰레드로 선택된 경우
  • 다른 쓰레드가 해당 Condition에 대해 signalAll 메서드를 호출한 경우
  • 다른 쓰레드가 현재 쓰레드를 인터럽트하고 쓰레드 중단의 인터럽트를 지원하는 경우
  • 의미 없는 깨어남(spurious wakeup) 발생하는 경우

 

위에 열거한 모든 경우에 await 메서드가 반환되기 전에 현재 쓰레드는 해당 Condition과 관련된 락을 다시 획득해야 하며 쓰레드가 반환되면 해당 락을 보유하는 것을 보장합니다.

또한, await 관련 메서드가 호출되는 시점에 현재 쓰레드는 해당 Condition과 관련된 락을 보유하고 있어야 하며 그렇지 않은 경우 IllegalMonitorStateException 예외가 발생합니다.

 

 

2.1 void await() throws InterruptedException

  • Condition.await() 메서드는 현재 쓰레드가 해당 Condition과 관련된 락을 해제하고 대기 상태로 만듦
  • 메서드 호출 시 쓰레드가 다음 상태 중 하나라면 InterruptedException이 발생하고 현재 쓰레드의 인터럽트 상태 초기화
    • 인터럽트 상태로 설정되어 있거나
    • 대기 중에 인터럽트 되는 경우

 

2.2 void awaitUninterruptibly()

  • Condition.await() 메서드는 현재 쓰레드가 해당 Condition과 관련된 락을 해제하고 대기 상태로 만듦
  • await()와 달리 메서드 호출 시 쓰레드가 인터럽트 상태로 설정되어 있거나 대기 중에 인터럽트 되더라도 시그널을 받을 때까지 계속 대기하며 현재 쓰레드의 인터럽트 상태를 유지
    • InterruptedException 발생 X

 

2.3 long awaitNanos(long nanosTimeout) throws InterruptedException

  • 지정된 나노초 값에 대한 남은 나노초의 추정치를 반환하거나 시간이 초과될 경우 0 이하 값을 반환
  • 지정된 나노초 시간이 경과할 때까지 대기
  • 메서드 호출 시 쓰레드가 다음 상태 중 하나라면 InterruptedException이 발생하고 현재 쓰레드의 인터럽트 상태 초기화
    • 인터럽트 상태로 설정되어 있거나
    • 대기 중에 인터럽트 되는 경우

 

2.4 boolean await(long time, TimeUnit uni) throws InterruptedException

  • 지정된 대기 시간이 경과할 때까지 대기
  • awaitNanos(unit.toNanos(time)) > 0과 같은 동작을 수행

 

2.5 boolean awaitUntil(Date deadline) throws InterruptedException

  • 지정된 마감 시간이 경과할 때까지 대기
  • 반환 값은 마감 시간이 경과했는지 여부를 나타냄

 

2.6 signal()

  • 대기 중인 쓰레드 하나를 깨우며 해당 Condtion에서 대기 중인 쓰레드가 있다면 그중 하나를 깨움
  • 꺠어난 쓰레드는 await에서 반환되기 전에 반드시 다시 락을 획득해야 함

 

2.7 signalAll()

  • 모든 대기 중인 쓰레드들을 깨우며 만약 어떤 쓰레드가 해당 Condition에 대해 대기 중이라면 모든 대기 중인 쓰레드들이 깨어남
  • 깨어난 각 쓰레드는 await에서 반환되기 전에 락을 획득해야 함
  • Condition에서 신호를 알릴 때 signalAll()보다 signal()을 사용하는 것이 다중 Condition을 다루는 더욱 효과적인 방법
    • 한 개의 Lock 객체에서 생성한 여러 개의 Condition은 특정 조건에 따라 쓰레드를 구분해서 관리하기 때문에 signal()을 통해 미세한 제어를 가능하게 해 줌
    • 여러 개의 Condition이 있을 때 모든 쓰레드를 동시에 깨우면 Race Condition이 발생할 수 있으나 Condition을 여러 개 사용하면 각각의 조건에 대해 필요한 쓰레드만 깨울 수 있음

 

3. Condition 사용 시 주의 사항

 

  • Condition 객체는 락과 조건 변수를 함께 제공하여 동기화를 구현하는 데 사용
  • Condition 객체는 독립적인 모니터를 가지고 있으며, 이는 Lock 객체와 별도로 작동.
  • Condition 객체의 모니터를 사용하면 다중 쓰레드 환경에서 데이터 접근 제어 가능
  • Condition 객체의 모니터를 사용할 때 주의해야 할 점
    • Lock 객체를 획득한 후에 Condition 객체의 메서드를 호출해야 함
    • Condition 객체의 메서드를 호출한 후에는 Lock 객체를 해제해야 함
    • Condition 객체의 모니터를 사용하는 것은 해당 Condition과 연결된 락을 사용하거나 await() 및 signal() 메서드를 사용하는 것과 연관 없음
      • 혼동을 피하기 위해 Condition 인스턴스를 이러한 방식으로 사용하지 않는 것을 권장
      • Condition 객체의 모니터를 사용하면 데드락이 발생할 수 있으므로 주의 필요


 

코드 부연 설명

    • 생산자 쓰레드
      • produce() 메서드는 큐에 데이터를 생산
      • 큐가 가득 차면 notFull.await() 메서드를 호출하여 대기
      • 큐에 공간이 생기면 데이터를 큐에 추가하고 notEmpty.signal() 메서드를 호출하여 소비자에게 알림

 

    • 소비자 쓰레드
      • consume() 메서드는 큐에서 데이터를 소비
      • 큐가 비어 있으면 notEmpty.await() 메서드를 호출하여 대기
      • 큐에 데이터가 추가되면 데이터를 큐에서 제거하고 notFull.signal() 메서드를 호출하여 생산자에게 알림

 

  • Condition이 모니터의 조건 변수보다 유연하여 동일한 락에 대해 두 가지 Condition인 notFull과 notEmpty를 가지는 것을 확인할 수 있었던 코드

 

참고

자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1] - 정수원 강사님

반응형