F-LAB JAVA · 4주차 · Phase 5 · 정교한 락: LockSupport와 ReentrantLock
🚀 Phase 5 시작 — 정교한 락 진입
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
synchronized 는 간단하고 자동 반환되는 장점이 있지만, (1) 무한 대기 (타임아웃 불가), (2) 인터럽트 불가, (3) 공정성 보장 X 라는 세 가지 핵심 한계를 가진다.
무한 대기 — 락을 획득할 때까지 무한정 BLOCKED 되며, "1초만 기다리고 포기" 같은 타임아웃을 설정할 수 없어, 락이 영원히 안 풀리면 영원히 대기한다.
인터럽트 불가 — synchronized 대기 중인 스레드는interrupt()로 깨울 수 없어, 응답성이 중요한 작업에서 문제가 된다.
공정성 보장 X — 락 획득 순서가 정의되지 않아 (비공정), 특정 스레드가 계속 락을 못 받는 기아 (starvation) 가 발생할 수 있다.
이러한 한계로 인해 데드락 발생 시 synchronized 로는 회복할 방법이 없으며, 자바는 이를 극복하는 정교한 락 도구 (Lock 인터페이스, ReentrantLock) 를 제공한다 (이번 Phase 의 주제).
synchronized = 단순한 화장실 (장점):
- 들어가면 자동으로 잠김
- 나오면 자동으로 풀림 (자동 반환)
- 단순함
한계 1 — 무한 대기:
- 안의 사람이 안 나오면
- 영원히 기다림 (포기 불가)
- "5분만 기다리고 다른 곳으로" 불가
한계 2 — 인터럽트 불가:
- 밖에서 문을 두드려도 (interrupt)
- 기다리는 사람은 안 나옴
- 급한 일 있어도 못 빠짐
한계 3 — 공정성 X:
- 줄 서도 순서 보장 X
- 새치기 가능
- 운 나쁘면 영원히 못 들어감 (기아)
ReentrantLock = 정교한 화장실 (Phase 5):
- 타임아웃 (tryLock)
- 두드리면 나옴 (lockInterruptibly)
- 줄 순서 (공정 모드)
→ synchronized 는 단순하지만 3가지 한계, ReentrantLock 이 극복.
1. synchronized의 장점 (먼저)
2. 한계 1 — 무한 대기
3. 한계 2 — 인터럽트 불가
4. 한계 3 — 공정성 X
5. 운영 환경의 무한 대기 사고
6. 데드락과 회복 불가
7. 더 정교한 락의 필요성
8. synchronized vs Lock 선택
9. 면접 + 자기 점검
synchronized 의 장점:
한계만 보기 전에 장점 인정.
1. 간단함
- 키워드만
- 학습 쉬움
2. 자동 반환
- unlock 불필요
- 예외 시에도 안전
3. 재진입
- 같은 스레드 재획득
4. 가독성
- 명확한 의도
// synchronized — 매우 간단
public synchronized void method() {
// 동기화됨
}
// 또는 블록
synchronized (lock) {
// 동기화됨
}
// 한 키워드로 동기화
// synchronized — 자동 반환 (예외 안전)
synchronized (lock) {
doWork();
throw new RuntimeException(); // 예외!
// 그래도 락 자동 반환
}
// vs ReentrantLock — 수동
lock.lock();
try {
doWork();
} finally {
lock.unlock(); // 명시적 (깜박하면 데드락)
}
synchronized 가 적합:
- 단순한 동기화
- 짧은 임계 영역
- 타임아웃/인터럽트 불필요
- 공정성 불필요
대부분의 단순한 경우 충분
@Service
public class SimpleSyncService {
private int counter = 0;
// synchronized — 단순, 충분
public synchronized void increment() {
counter++;
// 짧고 단순
// 타임아웃/인터럽트 불필요
// synchronized 적합
}
// 자동 반환 (예외 안전)
public synchronized void process(Shipment shipment) {
counter++;
validate(shipment); // 예외 가능, 락 자동 반환
}
private void validate(Shipment s) { }
}
synchronized의 장점은?
답:
1. 간단함:
자동 반환:
재진입:
적합:
무한 대기 (타임아웃 불가):
synchronized 진입 시 락 없으면
무한정 BLOCKED.
문제:
- 타임아웃 설정 불가
- "N초만 기다리고 포기" 불가
- 락 영원히 안 풀리면 영원히 대기
// synchronized — 무한 대기
public void process() {
synchronized (lock) { // 락 없으면 무한 BLOCKED
// 락 받을 때까지 영원히
doWork();
}
}
// 타임아웃 불가
// "5초만 시도하고 다른 일" 불가
무한 대기 문제:
스레드 A 가 락 보유 후 멈춤:
- 무한 루프
- 데드락
- 외부 자원 무한 대기
스레드 B (synchronized 대기):
- A 가 안 풀면 영원히 BLOCKED
- 포기 불가
- 응답 없음
// ReentrantLock — 타임아웃 가능
ReentrantLock lock = new ReentrantLock();
public boolean processWithTimeout() throws InterruptedException {
if (lock.tryLock(5, TimeUnit.SECONDS)) { // 5초만
try {
doWork();
return true;
} finally {
lock.unlock();
}
}
// 5초 내 못 얻으면
log.warn("Could not acquire lock");
return false; // 포기 (무한 대기 X)
}
무한 대기:
synchronized:
스레드 A: [락 보유, 멈춤────────────────]
스레드 B: [BLOCKED 영원히────────────────]
↑ 포기 불가
ReentrantLock (tryLock):
스레드 A: [락 보유, 멈춤────]
스레드 B: [tryLock 5초][포기→다른 일]
↑ 타임아웃
@Service
public class InfiniteWaitProblem {
private final Object lock = new Object();
// ❌ synchronized — 무한 대기
public void processSync(Shipment shipment) {
synchronized (lock) {
// 다른 스레드가 락 안 풀면 영원히 대기
callSlowExternalApi(shipment); // 만약 멈추면?
}
}
// ✓ ReentrantLock — 타임아웃
private final ReentrantLock reentrantLock = new ReentrantLock();
public boolean processWithTimeout(Shipment shipment) {
try {
if (reentrantLock.tryLock(10, TimeUnit.SECONDS)) {
try {
callSlowExternalApi(shipment);
return true;
} finally {
reentrantLock.unlock();
}
}
log.warn("Timeout for shipment {}", shipment.getId());
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private void callSlowExternalApi(Shipment s) { }
}
무한 대기의 문제는?
답:
1. 무한 대기:
문제:
시나리오:
해결:
인터럽트 불가:
synchronized 대기 (BLOCKED) 중인 스레드는
interrupt() 로 깨울 수 없음.
문제:
- 외부에서 못 깨움
- 응답성 ↓
- 종료 신호 무시
// synchronized — 인터럽트 안 통함
Thread worker = new Thread(() -> {
synchronized (lock) { // BLOCKED 대기 중
doWork();
}
});
worker.start();
worker.interrupt(); // ❌ BLOCKED 는 안 깨어남
// 락 받을 때까지 계속 대기
// 인터럽트 무시됨
인터럽트 불가 문제:
앱 종료 시:
- 스레드들에 인터럽트
- 정리하고 종료 유도
하지만 synchronized BLOCKED:
- 인터럽트 무시
- 락 받을 때까지 대기
- 종료 지연
응답성 중요한 경우:
- 사용자 취소
- 타임아웃
- 못 함
// ReentrantLock — 인터럽트 가능
ReentrantLock lock = new ReentrantLock();
public void processInterruptible() {
try {
lock.lockInterruptibly(); // 인터럽트 가능
try {
doWork();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 대기 중 인터럽트 → 여기로
Thread.currentThread().interrupt();
log.info("Interrupted while waiting for lock");
}
}
인터럽트 불가:
synchronized:
스레드: [BLOCKED 대기]
interrupt() → 무시 (계속 대기)
ReentrantLock (lockInterruptibly):
스레드: [대기]
interrupt() → InterruptedException (깨어남)
@Service
public class InterruptProblem {
private final Object lock = new Object();
// ❌ synchronized — 인터럽트 불가
public void processSync(Shipment shipment) {
synchronized (lock) { // BLOCKED 시 인터럽트 무시
doWork(shipment);
}
}
// ✓ ReentrantLock — 인터럽트 가능
private final ReentrantLock reentrantLock = new ReentrantLock();
public void processInterruptible(Shipment shipment) {
try {
reentrantLock.lockInterruptibly(); // 인터럽트로 깰 수 있음
try {
doWork(shipment);
} finally {
reentrantLock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.info("Processing cancelled for {}", shipment.getId());
// 사용자 취소, 종료 신호 등 처리 가능
}
}
private void doWork(Shipment s) { }
}
인터럽트 불가의 문제는?
답:
1. 인터럽트 불가:
문제:
해결:
활용:
공정성 (Fairness) 보장 X:
락 획득 순서가 정의되지 않음.
- 비공정 (Non-fair)
- 먼저 대기 ≠ 먼저 획득
- 새치기 가능
기아 (Starvation):
특정 스레드가 계속 락을 못 받음.
원인:
- 비공정 락
- 다른 스레드가 자주 받음
- 운 나쁜 스레드 무한 대기
문제:
- 특정 작업 지연
- 응답 없음
// synchronized — 비공정
public void process() {
synchronized (lock) { // 순서 보장 X
// 먼저 대기한 스레드가 먼저 받는다는 보장 없음
doWork();
}
}
// 스레드 A, B, C 경쟁:
// - A 가 자주 받을 수 있음
// - C 가 기아 가능
// ReentrantLock — 공정 모드
ReentrantLock fairLock = new ReentrantLock(true); // 공정
public void processFair() {
fairLock.lock(); // FIFO 순서 (먼저 대기 먼저 획득)
try {
doWork();
} finally {
fairLock.unlock();
}
// 기아 방지
// 단, 성능 ↓ (순서 관리 비용)
}
공정 vs 비공정:
공정 (Fair):
+ 순서 보장 (FIFO)
+ 기아 방지
- 성능 ↓ (순서 관리)
비공정 (Non-fair):
+ 성능 ↑ (처리량)
+ 기본값
- 기아 가능
선택:
- 순서 중요: 공정
- 성능 중요: 비공정 (기본)
@Service
public class FairnessProblem {
private final Object lock = new Object();
// ❌ synchronized — 비공정 (기아 가능)
public void processNonFair(Shipment shipment) {
synchronized (lock) {
// 순서 보장 X
doWork(shipment);
}
}
// ✓ ReentrantLock(true) — 공정 (FIFO)
private final ReentrantLock fairLock = new ReentrantLock(true);
public void processFair(Shipment shipment) {
fairLock.lock(); // 먼저 온 순서
try {
doWork(shipment);
// 모든 shipment 가 공평하게 처리
// 기아 방지
} finally {
fairLock.unlock();
}
}
private void doWork(Shipment s) { }
}
공정성 X의 문제는?
답:
1. 공정성 X:
기아:
해결:
트레이드오프:
운영 환경 무한 대기 사고:
시나리오:
- 스레드 A 가 락 보유 후
- 외부 API 호출 (응답 없음, 무한 대기)
- 다른 모든 스레드 BLOCKED
- 서버 전체 멈춤
결과:
- 응답 없음
- 스레드 풀 고갈
- 서비스 다운
스레드 풀 고갈:
톰캣 스레드 200개:
- 모두 같은 락 대기 (BLOCKED)
- 새 요청 처리 불가
- 503 에러
원인:
- 락 보유 스레드가 안 풀음
- 무한 대기 (synchronized)
→ 서비스 장애
# jstack 으로 진단
$ jstack <pid>
# 많은 스레드가 BLOCKED:
"http-thread-1" BLOCKED (on object monitor)
waiting to lock <0x...>
"http-thread-2" BLOCKED (on object monitor)
waiting to lock <0x...> (같은 락)
...
# 락 보유 스레드 확인:
"http-thread-99" RUNNABLE
- locked <0x...>
at SocketRead (외부 API 무한 대기)
# 이 스레드가 락 잡고 멈춤
// ❌ 위험 — 락 안에서 외부 호출
public void riskyProcess() {
synchronized (lock) {
callExternalApi(); // 응답 없으면 무한 대기
// 다른 스레드 모두 BLOCKED
}
}
// ✓ 개선 1 — 락 밖에서 외부 호출
public void betterProcess() {
Result result = callExternalApi(); // 락 밖 (병렬)
synchronized (lock) {
updateState(result); // 메모리 연산만
}
}
// ✓ 개선 2 — 타임아웃
public boolean timeoutProcess() throws InterruptedException {
if (reentrantLock.tryLock(5, TimeUnit.SECONDS)) {
try {
callExternalApiWithTimeout(); // 자체 타임아웃도
return true;
} finally {
reentrantLock.unlock();
}
}
return false; // 락 못 얻으면 포기
}
@Service
public class ProductionWaitIncident {
private final Object lock = new Object();
private final ReentrantLock reentrantLock = new ReentrantLock();
// ❌ 사고 패턴 — 락 안 외부 호출
public void dangerousPattern(Shipment shipment) {
synchronized (lock) {
// 외부 추적 API (응답 없으면?)
Tracking tracking = trackingApi.fetch(shipment.getBlNo());
// 만약 멈추면 모든 요청 BLOCKED → 서비스 다운
update(tracking);
}
}
// ✓ 안전 패턴
public boolean safePattern(Shipment shipment) {
// 외부 호출 락 밖 + 자체 타임아웃
Tracking tracking = trackingApi.fetchWithTimeout(
shipment.getBlNo(), Duration.ofSeconds(5));
// 락은 타임아웃 + 짧은 임계 영역
try {
if (reentrantLock.tryLock(3, TimeUnit.SECONDS)) {
try {
update(tracking); // 메모리 연산
return true;
} finally {
reentrantLock.unlock();
}
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private void update(Tracking t) { }
}
운영 환경 무한 대기 사고는?
답:
1. 사고:
스레드 풀 고갈:
진단:
예방:
데드락 (Deadlock):
두 스레드가 서로의 락을 기다리며
영원히 멈춤.
조건:
- 상호 배제
- 점유 대기
- 비선점
- 순환 대기
// 데드락 예제
Object lockA = new Object();
Object lockB = new Object();
// 스레드 1
synchronized (lockA) {
sleep(100);
synchronized (lockB) { // lockB 대기
// ...
}
}
// 스레드 2
synchronized (lockB) {
sleep(100);
synchronized (lockA) { // lockA 대기
// ...
}
}
// 스레드 1: lockA 보유, lockB 대기
// 스레드 2: lockB 보유, lockA 대기
// → 서로 대기 → 데드락
synchronized 데드락 회복 불가:
데드락 발생 시:
- 두 스레드 모두 BLOCKED
- 무한 대기 (타임아웃 X)
- 인터럽트 X (못 깨움)
- 회복 방법 없음
결과:
- 영원히 멈춤
- 재시작만이 답
데드락:
스레드 1: lockA 보유 ──→ lockB 대기 (BLOCKED)
↑
스레드 2: lockB 보유 ──→ lockA 대기 (BLOCKED)
↑
순환 대기 → 영원히
synchronized:
- 타임아웃 X → 포기 못 함
- 인터럽트 X → 못 깨움
→ 회복 불가
// ReentrantLock — tryLock 으로 데드락 회피
public boolean transfer(Account from, Account to, int amount)
throws InterruptedException {
if (from.lock.tryLock(1, TimeUnit.SECONDS)) {
try {
if (to.lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 둘 다 획득
from.balance -= amount;
to.balance += amount;
return true;
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
// 락 못 얻으면 포기 + 재시도
return false; // 데드락 회피
}
@Service
public class DeadlockRecovery {
// ❌ synchronized — 데드락 회복 불가
public void transferSync(ShipmentAccount from, ShipmentAccount to, BigDecimal amount) {
synchronized (from) {
synchronized (to) { // 순서 다르면 데드락
from.debit(amount);
to.credit(amount);
}
}
// 데드락 시 회복 불가
}
// ✓ ReentrantLock — tryLock 으로 회피
public boolean transferSafe(ShipmentAccount from, ShipmentAccount to, BigDecimal amount) {
try {
if (from.lock.tryLock(1, TimeUnit.SECONDS)) {
try {
if (to.lock.tryLock(1, TimeUnit.SECONDS)) {
try {
from.debit(amount);
to.credit(amount);
return true;
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
return false; // 락 못 얻음 → 재시도
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
데드락에서 synchronized가 회복 불가한 이유는?
답:
1. 데드락:
회복 불가:
결과:
해결:
// java.util.concurrent.locks.Lock
public interface Lock {
void lock(); // 기본 획득
void lockInterruptibly() throws ...; // 인터럽트 가능
boolean tryLock(); // 즉시 시도
boolean tryLock(long time, TimeUnit unit); // 타임아웃
void unlock(); // 반납
Condition newCondition(); // 조건
}
Lock 이 synchronized 한계 극복:
synchronized 한계 → Lock 해결:
1. 무한 대기 → tryLock(timeout)
2. 인터럽트 X → lockInterruptibly()
3. 공정성 X → new ReentrantLock(true)
4. 데드락 회복 X → tryLock
Lock 의 추가 기능:
- tryLock (즉시/타임아웃)
- lockInterruptibly
- 공정성 옵션
- Condition (정교한 대기/통지)
- 여러 Condition
→ 정교한 제어
// Lock 의 비용 — 명시적 unlock
Lock lock = new ReentrantLock();
lock.lock();
try {
// 임계 영역
} finally {
lock.unlock(); // ★ 필수 (깜박하면 데드락)
}
// synchronized 는 자동
// Lock 은 수동 (try-finally 필수)
Lock 의 구현체:
ReentrantLock:
- 재진입 락
- synchronized 대체
- Unit 5.3
ReadWriteLock / ReentrantReadWriteLock:
- 읽기/쓰기 분리
- 읽기는 공유
StampedLock:
- 낙관적 읽기
- Java 8+
@Service
public class LockNecessity {
// synchronized 한계 → Lock 으로
private final ReentrantLock lock = new ReentrantLock();
// 1. 타임아웃
public boolean withTimeout(Shipment s) throws InterruptedException {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try { process(s); return true; }
finally { lock.unlock(); }
}
return false;
}
// 2. 인터럽트
public void interruptible(Shipment s) throws InterruptedException {
lock.lockInterruptibly();
try { process(s); }
finally { lock.unlock(); }
}
// 3. 즉시 시도
public boolean tryNow(Shipment s) {
if (lock.tryLock()) {
try { process(s); return true; }
finally { lock.unlock(); }
}
return false; // 이미 처리 중
}
private void process(Shipment s) { }
}
더 정교한 락의 필요성은?
답:
1. Lock 인터페이스:
한계 극복:
비용:
구현체:
| 항목 | synchronized | Lock (ReentrantLock) |
|---|---|---|
| 타임아웃 | X | O (tryLock) |
| 인터럽트 | X | O (lockInterruptibly) |
| 공정성 | X | O (옵션) |
| 반환 | 자동 | 수동 (unlock) |
| 간단함 | O | △ (try-finally) |
| Condition | wait/notify | 여러 Condition |
synchronized 선택:
✓ 단순한 동기화
✓ 짧은 임계 영역
✓ 타임아웃/인터럽트 불필요
✓ 가독성 우선
대부분의 단순한 경우
Lock 선택:
✓ 타임아웃 필요
✓ 인터럽트 필요
✓ 공정성 필요
✓ 정교한 제어 (Condition)
✓ tryLock (데드락 회피)
복잡한 동시성 제어
권장:
먼저 synchronized 고려:
- 간단하면 충분
- 자동 반환 안전
Lock 필요 시:
- 타임아웃/인터럽트/공정성
- 정교한 제어
또는 더 높은 추상화:
- Atomic
- 동시성 컬렉션
- Executor (Phase 7)
현대적 관점:
대부분의 경우:
- 직접 락 < 고수준 도구
우선순위:
1. 무상태 (락 불필요)
2. 불변 객체
3. 동시성 컬렉션
4. Atomic
5. synchronized
6. Lock (정교한 제어 시)
@Service
public class LockChoiceGuide {
// 1. synchronized — 단순
private int simpleCounter = 0;
public synchronized void simple() {
simpleCounter++;
}
// 2. ReentrantLock — 정교한 제어
private final ReentrantLock lock = new ReentrantLock();
public boolean withControl(Shipment s) throws InterruptedException {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try { process(s); return true; }
finally { lock.unlock(); }
}
return false;
}
// 3. Atomic — 단일 변수 (권장)
private final AtomicInteger atomicCounter = new AtomicInteger();
public void atomic() {
atomicCounter.incrementAndGet();
}
// 4. 동시성 컬렉션 (권장)
private final Map<Long, Shipment> cache = new ConcurrentHashMap<>();
public void cache(Shipment s) {
cache.put(s.getId(), s);
}
private void process(Shipment s) { }
}
synchronized vs Lock 선택은?
답:
1. synchronized:
Lock:
권장:
현대:
| Q | 핵심 답변 |
|---|---|
| synchronized 한계 3가지? | 무한 대기, 인터럽트 X, 공정성 X |
| 무한 대기? | 타임아웃 불가 |
| 인터럽트 불가? | BLOCKED 못 깸 |
| 공정성 X? | 기아 가능 |
| 운영 사고? | 스레드 풀 고갈 |
| 데드락 회복? | synchronized 불가 |
| synchronized 장점? | 간단, 자동 반환 |
| Lock 해결? | tryLock, lockInterruptibly |
| 언제 synchronized? | 단순 |
| 언제 Lock? | 정교한 제어 |
답:
답:
답:
답:
답:
1. synchronized 한계 3가지
2. 사고와 회복
3. 극복과 선택
이번 Unit에서 synchronized 한계를 봤다면, 다음은 LockSupport (저수준 도구).
🚀 Phase 5 — 정교한 락: LockSupport와 ReentrantLock
✅ Unit 5.1 synchronized의 한계 정리 ← 여기
⏭ Unit 5.2 LockSupport
⏭ Unit 5.3 ReentrantLock
⏭ Unit 5.4 tryLock (★ 마스터)
✅ Phase 1~4 (17 Unit, 1차 정점 완료)
🚀 Phase 5 — Lock 도구 (1/4 진행)
총: 18/35 Unit
🚀 Phase 5 시작 — 정교한 락 진입