List
Search
멀티스레드 환경에서 공유 자원을 안전하게 다루려면 동기화(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() | Sleep은 락 반납 안 함 → 교착 위험 | |||
synchronized + wait/notify | 올바른 락 반납·조건 대기 가능 | |||
ReentrantLock + Condition | 생산자/소비자 분리 가능 | |||
BlockingQueue | 모든 메커니즘을 캡슐화 |
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의 한계를 이해하고 사용할 것)