Search

멀티 스레드와 동시성 (5) 자바 동기화 메커니즘: 스레드 락 / 대기 집합 / 임계영역

Tags
Study
Java
Last edited time
2025/10/13 00:50
2 more properties
Search
멀티 스레드와 동시성 (6) 스레드풀과 Executor 프레임워크
Study
Java
멀티 스레드와 동시성 (6) 스레드풀과 Executor 프레임워크
Study
Java
멀티스레드 환경에서 공유 자원을 안전하게 다루려면 동기화(synchronization)가 필수입니다.
여기서는 스레드 락, 락 대기 집합(1차), 스레드 대기 집합(2차), 임계영역이라는 네 가지 핵심 개념을 정리합니다.

1. 스레드 락 (Thread Lock)

1.1. 역할

하나의 임계영역에 동시에 단 하나의 스레드만 들어오게 보장
경쟁 조건(race condition) 방지, 불변식 유지

1.2. 특징

메모리 가시성
synchronized / lock()/unlock() 경계는 happens-before 관계를 만들어 임계영역 내 쓰기가 다른 스레드에 보이도록 보장
재진입성(Reentrancy)
동일 스레드가 동일 락을 중첩 획득 가능 (호출 깊이만큼 카운트)

1.3. 종류

1.
모니터락 (synchronized)
synchronized(obj) 구문 사용 → obj 내장 모니터락 획득.
스레드가 못 들어오면 상태는 BLOCKED
2.
명시적 락 (ReentrantLock)
lock()/unlock() 직접 호출.
공정성(fair), tryLock(), lockInterruptibly() 등 옵션 제공.
내부적으로 AQS 큐 + LockSupport.park() 사용 → WAITING/TIMED_WAITING 상태가 흔함

2. 락 대기 집합 (Lock Wait Set, 1차 대기소)

2.1. 역할

락 자체를 얻기 위해 줄 서는 곳.
조건(wait/await)은 고려하지 않고 임계영역 진입만 기다림

2.2. 동작 방식

모니터락 (synchronized)
모니터의 entry set에서 대기
상태: BLOCKED
락이 풀리면 JVM이 후보를 깨워 경쟁
ReentrantLock (AQS)
AQS의 sync queue에서 대기
상태: WAITING/TIMED_WAITING
공정 모드: FIFO 성향 강화, 비공정 모드: 성능 유리

3. 스레드 대기 집합 (Thread Wait Set, 2차 대기소)

3.1. 역할

이미 임계영역에 들어왔으나, 조건이 충족되길 기다리는 곳
이때는 락을 반납하고 기다린다 → 다른 스레드가 조건을 바꿀 기회를 얻음

3.2. 동작 방식

모니터락 (synchronized)
객체마다 단일 wait set
notify() → 임의 스레드 하나 깨움 (생산자→생산자 가능 → 비효율/기아)
notifyAll() → 전부 깨움 (안전하지만 문맥 전환 비용↑)
ReentrantLock + Condition
Condition별 독립 대기 큐 보유
예: notFull(생산자 대기), notEmpty(소비자 대기) → 정확한 상대만 깨워 불필요 경쟁/기아 해소
await()은 락 반납 후 Condition 큐 대기, signal()은 해당 Condition 큐 대기자를 깨워 락 대기 집합(1차) 으로 이동
시간제한, 인터럽트 친화적 메서드 제공

4. 임계영역 (Critical Section)

4.1. 정의

공유 불변식을 지켜야 하는 코드 구간
락으로 보호되며 원자성 확보
예: 0 ≤ size ≤ capacity

4.2. 권장사항

1.
최소화: 짧고 단순하게 작성 (락 보유 시간↓)
2.
외부 호출 금지: I/O, 네트워크 호출은 락 밖에서
3.
Sleep 금지: Thread.sleep()은 락을 반납하지 않아 교착 유발
4.
조건 사용 규율: while 재검증 필수
5.
가시성 확보: 락 경계가 happens-before를 보장

5. 락 대기 vs 스레드 대기 비교

구분
언제 들어가는가
상태
예시
락 대기 집합 (1차)
임계영역에 진입하려다 락을 못 얻었을 때
BLOCKED (synchronized) WAITING (ReentrantLock)
- 모니터 entry set - AQS sync queue
스레드 대기 집합 (2차)
임계영역에 들어왔지만 조건이 불만족일 때
WAITING / TIMED_WAITING
- 모니터 wait set - Condition 큐

6. 흐름(상태 전이)

1.
스레드 → 임계영역 진입 시도
성공 → 임계영역 실행
실패 → 락 대기 집합(1차)
2.
임계영역에서 조건 불만족 → wait()/await() 호출
락 반납 + 스레드 대기 집합(2차) 이동
3.
notify/signal 수신 → 2차 → 1차 이동
4.
락 획득 성공 → 조건 재검증(while) 후 실행
5.
작업 종료 → 락 해제
flowchart LR
  classDef area fill:#fff,stroke:#333,rx:8,ry:8
  classDef dashed fill:#fff,stroke:#c33,stroke-dasharray:6 4,rx:8,ry:8,color:#c33
  classDef wait fill:#eef,stroke:#55a,rx:10,ry:10
  classDef lock fill:#fff,stroke:#333,rx:8,ry:8

  subgraph System["공유 객체/구조"]
    direction TB
    L["스레드 락"]:::lock
    subgraph LQ["락 대기 집합 (1차)</br>락 획득 대기"]
      BQ["BLOCKED / WAITING"]
    end

    subgraph CS["임계영역"]
      direction TB
      R["공유 상태/큐"]
      subgraph WQ["스레드 대기 집합 (2차)</br>조건 대기"]
        W1["WAITING / TIMED_WAITING"]
      end
    end
  end

  T["실행 스레드"] -->|락 시도| L
  L -->|성공| CS
  L -->|실패| LQ
  CS -->|조건 false → wait/await| WQ
  WQ -->|notify/signal| LQ
  LQ -->|락 재획득| CS
  CS -->|unlock| L
Mermaid
복사
flowchart LR
  %% 좌우 여백
  classDef box fill:#fff,stroke:#333,rx:6,ry:6
  classDef area fill:#fff,stroke:#333,stroke-width:1.5px,rx:8,ry:8
  classDef dashed fill:#fff,stroke:#c33,stroke-dasharray: 6 4,rx:8,ry:8,color:#c33
  classDef wait fill:#eef,stroke:#88f,rx:12,ry:12,color:#000
  classDef ext fill:#ffbf00,stroke:#c90,rx:16,ry:16,color:#000

  %% 외곽 컨테이너 (BoundedQueue)
  subgraph BQ[BoundedQueue]
  direction LR

    %% 왼쪽/오른쪽 API
    TAKE[[take]]:::box
    PUT[[put]]:::box

    %% 가운데 스택 배치용 컬럼
    subgraph MID[x001]
    direction TB

      %% lock, 락 대기 집합
      LOCK([🔒 lock]):::box
      LWAIT[락 대기 집합]:::area

      %% 임계영역 테두리(점선)
      subgraph CRIT[임계 영역]
      direction TB
      class CRIT dashed

        %% 큐 슬롯 2개
        subgraph Q[queue]
        direction LR
          S1([1]):::box
          S2([2]):::box
        end

        %% 스레드 대기 집합 (단일 wait set)
        subgraph WSET[스레드 대기 집합]
        direction LR
          C1((c1<br>WAITING)):::wait
          C2((c2<br>WAITING)):::wait
          C3((c3<br>WAITING)):::wait
        end
      end
    end
  end

  %% 외부 생산자들
  P1((p1<br>data1)):::ext
  P2((p2<br>data2)):::ext
  P3((p3<br>data3)):::ext

  %% 배선
  P1 --> PUT
  P2 --> PUT
  P3 --> PUT
  TAKE --> BQ
Mermaid
복사

7. 단계별 비교 요약

단계
스레드 락
1차 대기소
2차 대기소
비고
동기화 없음
레이스 조건 발생
synchronized + sleep()
모니터락
BLOCKED
Sleep은 락 반납 안 함 → 교착 위험
synchronized + wait/notify
모니터락
BLOCKED
wait set (단일)
올바른 락 반납·조건 대기 가능
ReentrantLock + Condition
명시적 락
AQS 큐 (WAITING)
여러 Condition 큐
생산자/소비자 분리 가능
BlockingQueue
내부 ReentrantLock
내부
내부
모든 메커니즘을 캡슐화

8. 자주 헷갈리는 포인트

BLOCKED vs WAITING
BLOCKED: 락 획득 대기(모니터 entry set)
WAITING: 조건 대기(wait set, Condition 큐)
미싱 시그널
while-await 패턴을 지키면 안전
ReentrantLock ≠ 모니터락
구현 기반(AQS) 다름, 대신 Condition 분리·tryLock 등 제어력 제공

9. 결론

락 대기 집합(1차) = “락 문 앞 대기열”
스레드 대기 집합(2차) = “조건 기다림 대기실”
언제 무엇을 쓰나?
가장 권장: BlockingQueue 같은 동시성 컬렉션(내부에 ReentrantLock/Condition 포함)
직접 제어 필요: 조건 큐 분리·정밀한 대기 정책이 필요하면 ReentrantLock + Condition
단순·소규모: synchronized + wait/notify 가능(단, 단일 wait set의 한계를 이해하고 사용할 것)