둘 이상의 작업이 서로가 가진 자원을 기다리며 영원히 멈추는 상태(교착 상태).
가장 단순한 형태
스레드 A: lock(1) 성공 → lock(2) 요청 → 대기 스레드 B: lock(2) 성공 → lock(1) 요청 → 대기
A는 B가 2를 놓기를, B는 A가 1을 놓기를 기다린다 → 영원히
성립 조건 네 가지 — 하나만 깨면 안 생긴다
| 조건 | 뜻 | 깨는 방법 |
|---|---|---|
| 상호 배제 | 자원을 동시에 못 씀 | 공유 가능하게(읽기 전용, 불변 객체) |
| 점유와 대기 | 가진 채로 더 요구 | 한 번에 다 잡거나, 못 잡으면 전부 놓기 |
| 비선점 | 강제로 뺏을 수 없음 | 타임아웃 후 포기 |
| 순환 대기 | 대기가 원을 이룸 | 자원에 순서를 매겨 그 순서로만 잡기 ★ |
★ 네 번째를 깨는 것이 실무에서 가장 현실적이다.
// ❌ 위험 — 호출자에 따라 순서가 달라진다
void transfer(Account from, Account to) {
synchronized (from) { synchronized (to) { … } }
}
// transfer(A, B)와 transfer(B, A)가 동시에 → 데드락
// ✅ id 순서로 고정 — 누가 부르든 항상 같은 순서
void transfer(Account a, Account b) {
Account first = a.id < b.id ? a : b;
Account second = a.id < b.id ? b : a;
synchronized (first) { synchronized (second) { … } }
}
다루는 세 가지 전략
① 예방(prevention) — 위 네 조건 중 하나를 애초에 못 만들게 한다. 가장 실용적. ② 회피(avoidance) — 은행원 알고리즘. 자원 요청마다 안전한지 검사한다. 이론적으로 우아하지만 최대 요구량을 미리 알아야 해서 실무에선 거의 안 쓴다. ③ 탐지와 복구(detection) — 일단 두고, 주기적으로 대기 그래프에서 사이클을 찾아 하나를 강제 종료한다. DB가 쓰는 방식이다.
DB 데드락 — 실무에서 훨씬 자주 만난다
-- 트랜잭션 1
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 트랜잭션 2 (동시에)
UPDATE account SET balance = balance - 50 WHERE id = 2;
UPDATE account SET balance = balance + 50 WHERE id = 1;
InnoDB는 대기 그래프에서 사이클을 감지해 비용이 적은 쪽을 롤백한다.
ERROR 1213: Deadlock found when trying to get lock; try restarting transaction
"try restarting transaction" — DB가 대놓고 재시도하라고 한다. 데드락은 완전히 없앨 수 없으므로 재시도 로직이 정답이다.
빈도를 줄이는 방법
- 갱신 순서를 일관되게(id 오름차순)
- 트랜잭션을 짧게
- 인덱스를 잘 걸어 락 범위를 좁게 (인덱스가 없으면 넓게 잠겨 충돌↑)
- 격리수준을 낮춰 갭 락을 줄이는 것도 선택지
진단
SHOW ENGINE INNODB STATUS; -- LATEST DETECTED DEADLOCK 섹션
어떤 두 트랜잭션이 어떤 락을 두고 충돌했는지 SQL까지 나온다.
자바는 jstack <pid> 가 "Found one Java-level deadlock" 을 직접 알려 준다.