JAVA/비동기 프로그래밍

[Java] 동기화 기법

꾸준함. 2024. 3. 4. 09:02

개요

앞선 게시물인 [Java] 동기화 개요를 읽고 해당 게시물을 읽는 것을 추천드립니다.

 

1. 뮤택스 (Mutual Exclusion)

  • 공유 자원에 대한 경쟁 상태를 방지하고 동시성 제어를 위한 락 메커니즘
  • 쓰레드가 임계 영역에서 Mutex 객체의 flag를 소유해 락을 획득하면 다른 쓰레드가 접근할 수 없으며 Mutex 객체 flag가 해제 즉, 락이 해제되어야지만 타 쓰레드가 임계 영역에 접근 가능
  • 정리하자면 Mutex 락을 가진 오직 한 개의 쓰레드만이 임계 영역에 진입할 수 있으며 락을 획득한 쓰레드만이 락 해제 가능

 

 

 

코드 부연 설명

  • 임계 영역을 점유하고 있는 쓰레드가 없을 경우 lock은 false
  • lock이 false인 상태에서 쓰레드가 임계 영역에 접근하고자 acquired() 메서드를 호출하면 락을 획득하여 lock 값이 true
    • 락을 획득한 쓰레드가 release() 메서드를 통해 락을 해제하지 않는 이상 임계 영역에 접근하는 쓰레드들은 while문을 빠져나오지 못하고 대기

 

1.1 뮤택스 문제점

 

뮤택스 이용 시 세 가지 문제점이 발생할 수 있습니다.

  • 데드락
  • 우선순위 역전
  • 오버헤드로 인한 성능 저하

 

1.1.1 데드락

  • 두 개 이상의 쓰레드가 서로가 가진 락을 기다리면서 상호적으로 블로킹되어 아무 작업도 수행할 수 없는 상태
  • 뮤택스를 적절히 사용하지 않거나 잘못된 순서로 락을 해제하는 경우 데드락 발생 가능

 

1.1.2 우선순위 역전

  • 높은 우선순위를 가진 쓰레드가 낮은 우선순위를 가진 쓰레드가 보유한 락을 기다리는 동안 블록되는 현상
  • 스케줄러가 높은 우선순위를 가진 쓰레드를 우선 실행하므로 스케줄러가 낮은 우선순위를 가진 쓰레드를 실행하여 락을 해제할 때까지 지연될 수 있음
  • 위 문제는 우선순위 상속을 통해 해결 가능
    • 낮은 우선순위를 가진 쓰레드가 락을 보유하고 있을 때, 높은 우선순위를 가진 다른 쓰레드가 해당 락을 획득하려고 할 때, 낮은 우선순위 쓰레드의 우선순위를 일시적으로 높여주는 것
    • 자바에서는 ReentrantLock이 우선순위 상속을 지원하므로 ReentrantLock을 이용해 락을 구현하면 위 문제 해결

 

 

 

1.1.3 오버헤드로 인한 성능 저하

  • 뮤택스를 사용하면 여러 쓰레드가 경합하면서 락을 얻기 위해 쓰레드 스케줄링 발생
  • 이로 인해 오버헤드 발생하고 성능 저하 가능

 

2. 세마포어 (Semaphore)

  • 공유 자원에 대한 접근을 제어하기 위해 사용되는 신호전달 메커니즘 동기화 도구
  • 정수형 변수 S와 P(), V()의 두 가지 원자적 함수로 구성된 신호전달 메커니즘 동기화 도구
    • S는 공유 자원의 개수로서 이 개수만큼 쓰레드의 접근을 허용 (Mutex는 1개만 허용)
    • P()는 임계 영역을 사용하려는 쓰레드의 진입 여부를 결정하는 연산으로 Wait 연산
    • V()는 대기 중인 프로세스를 깨우는 신호로 Signal 연산

 

  • 자바에서는 java.util.concurrent 패키지에 세마포어 구현체를 포함하고 있기 때문에 직접 구현할 필요 없음

 

2.1 세마포어 유형

 

세마포어는 카운트 변수 S가 1인 Binary Semaphore와 2 이상의 양수 값을 가지는 Counting Semaphore로 구분할 수 있습니다.

 

2.1.1 Binary Semaphore

  • 세마포어를 뮤텍스처럼 락으로 사용하기 위해 카운트 변수를 1로 설정하고 한 쓰레드 안에서 획득하고 해제


 

2.1.2 Counting Semaphore

  • 카운트 변수를 설정해 쓰레드가 공유할 수 있는 자원의 최대치를 한정해서 운용하는 방식으로 ThreadPool이나 DBConnectionPool 같은 자원 풀이나 컬렉션의 크기에 제한을 두고자 할 때 유용
    • ex) DB Connection 개수 제한, 파일 다운로드 동시 실행 제한

 

  • 락을 획득한 쓰레드와 해제하는 쓰레드가 다를 수 있음
  • 락과 락 해제를 위한 신호를 전달함으로써 동기화를 구현


 

2.2 뮤택스 vs 세마포어

 

  뮤택스 세마포어
동작 방식 공유 자원에 대한 접근을 동시에 하나의 쓰레드만 가능하도록 보장 특정 개수의 쓰레드가 동시에 공유 자원에 접근할 수 있도록 제어
(Binary Semaphore는 뮤택스와 유사한 역할)
소유권 뮤택스는 소유권이 있어서 락을 획득한 쓰레드만이 락 해제 가능 세마포어는 소유권이 없으며, 특정 개수의 쓰레드가 동시에 접근을 허용하는 카운팅 기법으로 동작

세마포어를 사용하는 쓰레드들이 모두 세마포어를 해제 가능
초기값 기본적으로 잠겨있는 상태로 시작

한 쓰레드가 뮤택스를 획득하여 자원에 접근 시 다른 쓰레드들은 뮤택스를 획득하기 위해 블로킹
초기값을 설정할 수 있으며 초기값에 따라서 처음부터 쓰레드가 자원에 접근할 수 있는지 여부가 결정
사용 목적 하나의 자원에 하나의 쓰레드만 접근하도록 보장해야 하는 경우에 사용 리소스의 한정적인 사용을 제어하는 데 사용되며 특정 개수의 쓰레드만이 동시에 자원에 접근하도록 제한하고자 할 때 사용

 

3. 모니터

  • 자바가 동기화를 지원하기 위해 사용하는 메커니즘
  • 뮤택스나 세마포어보다 고수준의 동기화 기법
  • 자바의 모니터는 상호 배제와 협력이라는 두 가지 동기화 기능 제공
    • 이를 위해 뮤택스와 조건 변수 사용

 

3.1 상호 배제(Mutual Exclusion)

 

  • 여러 쓰레드가 동시에 공유 자원에 접근하는 것을 막아 데이터의 일관성 및 안전성을 보장하는 메커니즘
  • JVM은 synchronized 키워드를 이용하여 뮤택스 동기화를 암묵적으로 처리

 

3.2 협력(Cooperation)

 

  • 어찌 보면 상호 배제와 상반되는 개념
  • 모니터의 조건 변수를 통해 다수의 쓰레드가 공유 자원을 안전하게 사용할 수 있도록 함께 작업하는 것
  • 모니터의 조건 변수란
    • Object 클래스의 메서드인 wait(), notify(), notifyAll()과 함께 작용하며 특정 조건이 만족될 때까지 쓰레드를 대기시키는 기능 제공
    • 쓰레드가 특정 조건에 부합하지 않을 때 wait() 메서드를 호출하면 조건 변수의 Wait Set에 들어가 대기
    • 다른 쓰레드가 특정 조건을 만족해 notify() 혹은 notifyAll() 메서드를 호출하면 해당 조건 변수의 Wait Set으로부터 쓰레드들을 깨워 실행시킴
    • 조건 변수를 통해 race condition 해결 가능

 

  • 원칙적으로 모니터 내부에 여러 개의 조건 변수를 가질 수 있지만 자바의 모니터는 오직 한 개의 조건 변수만 지닐 수 있음

 

3.3 모니터의 대기 자료 구조

 

  • 자바의 모니터 내부에는 Entry Set과 Wait Set이라는 대기 자료 구조가 존재
  • 이들은 멀티 쓰레드 환경에서 쓰레드들 간의 상호작용을 조절하는 데 사용

 

https://medium.com/javarevisited/whats-a-monitor-in-java-8f0ebecaea2a

 

3.3.1 Entry Set

  • 모니터의 락을 획득하기 위해 대기 중인 쓰레드들을 모아 놓은 자료구조
  • 쓰레드가 락을 사용 중인 경우 그 외 다른 쓰레드는 Entry Set에 들어감

 

3.3.2 Wait Set

  • 모니터의 조건 변수와 함께 사용하는 자료구조
  • 쓰레드들이 특정한 조건을 만족할 때까지 대기하고 있는 장소
  • 쓰레드는 Wait Set에 들어가 대기할 때 락을 해제하며
  • 다른 쓰레드에 의해 깨어나게 되면 Entry Set으로 이동해 다시 락 획득 시도

 

3.4 조건 변수 종류

 

  • 조건 변수를 통해 상호 협력하고 있는 두 쓰레드가 wait()과 notify() 메서드 실행 후 하나의 모니터를 두고 두 쓰레드 모두 소유가 가능한 상황 발생
    • 하나는 대기 중인 쓰레드
    • 나머지 하나는 깨우는 쓰레드

 

  • 어떤 쓰레드가 모니터를 먼저 소유할 것인가에 따라 두 종류의 조건 변수로 나눌 수 있음
    • Signal and Wait
    • Signal and Continue

 

  • 자바는 Signal and Continue 조건 변수를 사용

 

3.4.1 Signal and Wait

  • 모니터를 소유하고 있는 쓰레드가 wait() 메서드 실행 시 모니터 내부에 돌고 있는 자신을 중단하고 락을 해제 후 Wait Set에 들어감
  • 깨우는 쓰레드가 notify() 혹은 notifyAll() 명령을 실행해 Wait Set에 대기 중인 쓰레드 혹은 쓰레드들을 깨우고 깨우는 쓰레드는 락을 해제하고 대기
  • 대기에서 깨어난 쓰레드가 락 획득 후 모든 작업을 마치고 락을 해제하면 깨운 쓰레드가 락을 획득한 후 계속 작업 진행
  • 대기 쓰레드와 깨운 쓰레드 사이에 다른 쓰레드가 모니터를 소유할 수 없도록 원자적 실행 보장 필요

 

3.4.2 Signal and Continue (자바에서 채택한 조건 변수)

  • 모니터를 소유하고 있는 쓰레드가 wait() 메서드 실행 시 모니터 내부에 돌고 있는 자신을 중단하고 락을 해제 후 Wait Set에 들어감
  • 깨우는 쓰레드가 notify() 혹은 notifyAll() 명령을 실행해 Wait Set에 대기 중인 쓰레드 혹은 쓰레드들을 깨우고 일어난  쓰레드들은 Entry Set으로 이동
  • Signal and Wait과 달리 깨운 쓰레드가 락을 계속 유지하면서 모든 작업을 완료하고 락을 해제
  • 이후 Entry Set 내 모든 쓰레드가 락을 획득하기 위해 경쟁

 

3.5 자바 모니터 작동 방식

 

  • 쓰레드가 모니터 영역에 진입하기 위해 synchronized 메서드를 호출하면 모니터 작동
  • 쓰레드는 모니터 영역 진입을 위해 Entry Set에 입장 후 모니터 락 획득 시도
  • Entry Set에 이미 대기하고 있거나 현재 모니터를 소유한 쓰레드가 없을 경우 즉시 모니터의 소유자가 되어 모니터 영역 진입
    • 만약 다른 쓰레드가 이미 모니터 영역을 점유한 상태라면 모니터 영역에 진입하지 못하고 Entry Set으로 들어가 대기

 

  • Wait Set에 대기 중인 쓰레드는 모니터 쓰레드가 모니터를 해제할 때까지 지속 대기
  • 모니터 쓰레드가 모니터를 해제할 수 있는 경우는 두 가지
    • 실행 중인 모니터 영역 완료
    • wait 명령 실행

 

  • 모니터 쓰레드가 어떤 조건에 부합하지 않아 wait() 메서드 실행하면 모니터가 해제되고 Wait Set에 들어가 대기
    • 이때 다른 쓰레드가 모니터를 소유한 상태에서 만약 notify()를 실행하지 않고 그냥 종료하는 경우 Wait Set의 쓰레드가 깨어나지 않기 때문에 Entry Set의 쓰레드들만 모니터를 두고 경쟁
    • 만약 모니터 쓰레드가 notify()를 실행하면 Wait Set에 대기 중인 모든 쓰레드를 깨우고 이들은 Entry Set으로 이동
    • 쓰레드가 깨어나는 즉시 모니터를 소유하는 것이 아니고 락 획득 위해 경쟁

 

  • 모니터 쓰레드는 모니터를 계속 소유한 상태에서 모니터 영역을 실행하고 종료 (Signal and Continue)
    • 이때 Entry Set과 Wait Set에서 이동한 모든 쓰레드가 모니터를 두고 경쟁하게 되는데 만약 조건 변수 대기에서 깨어난 쓰레드 중에서 모니터를 소유했는데 해당 시점에 또다시 조건이 맞지 않을 경우 다시 wait() 메서드를 호출하고 대기 상태로 들어갈 수도 있음

 

  • Entry Set과 Wait Set에서 다음 쓰레드를 선택하는 기준은 오직 OS 스케줄러에 의해 결정

 

4. Spin Lock

  • 뮤택스나 세마포어와 같은 동기화 기법의 일종
  • 대기하지 않고 쓰레드가 임계 영역을 사용할 수 있을 때까지 끊임없이 반복하여 검사하는 동기화 메커니즘


 

코드 부연 설명

  • AtomicBoolean을 사용하여 락의 상태를 나타내고
  • compareAndSet메서드를 사용하여 락을 시도
  • 만약 다른 스레드가 이미 락을 획득한 상태라면, 스핀락이 발생하여 해당 스레드가 락을 해제할 때까지 반복

 

4.1 Busy Waiting

 

  • 스핀락에서 어떤 쓰레드가 락 획득 시도를 반복하면서 대기하는 상태
  • 쓰레드가 특정 조건을 기다리는 동안 어떠한 유용한 작업을 수행하지 않고, 무한 반복 루프를 돌며 CPU 자원을 계속 사용

 

4.2 Spin Lock 장단점

 

장점

  • 쓰레드가 공유 자원을 얻을 때까지 블로킹하지 않고 반복 검사하므로 context switching 비용 감소
  • 블로킹 대기 없이 공유 자원 접근을 시도하기 때문에 임계 영역 내 대기 시간이 짧을 경우 자원을 덜 소모함

 

단점

  • 스핀락은 다른 스레드가 락을 해제할 때까지 계속 반복하기 때문에, 대기 시간이 길어질 경우 효율이 떨어짐
  • 무한 루프로 인한 CPU 리소스 낭비
    • 공유 자원에 대한 경쟁이 심할 경우 리소스 낭비가 심해짐 
    • 싱글 코어일 경우 쓰레드가 무한 루프를 돌면서 다른 쓰레드가 CPU를 점유할 기회를 주지 않기 때문에 싱클 코어 환경에서는 스핀락 사용을 권장하지 않음

 

* 대부분의 경우 스핀락보다는 뮤택스나 세마포어와 같이 블로킹 기반의 동기화 기법을 사용하는 것이 적합할 가능성이 높습니다.

 

참고

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

반응형