이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › 스레드와 동시성
교착 상태
서로 기다리다 둘 다 멈춘다
스레드 1은 계좌 A에서 B로, 스레드 2는 B에서 A로 이체합니다. 이체하려면 두 계좌의 락을 모두 잡아야 합니다. 락을 잡는 순서와 타이밍에 따라 두 스레드가 서로의 락을 기다리며 영원히 멈출 수 있습니다.
실행으로 대조 · JDK 21에서 실행해 확인
이 실습에 나오는 개념
- 교착 상태(Deadlock)
- 둘 이상의 스레드(또는 트랜잭션)가 서로 상대가 가진 자원을 기다리느라 영원히 진행하지 못하는 상태다. A가 자원 1을 잡고 2를 기다리는데 B가 자원 2를 잡고 1을 기다리는 경우가 전형적이다. 자원을 항상 같은 순서로 잡도록 하면 예방할 수 있다.
- 임계 영역과 상호 배제
- 임계 영역은 여러 스레드가 동시에 실행하면 문제가 생기는 코드 구간이다. 상호 배제는 그 구간을 한 번에 하나의 스레드만 실행하도록 보장하는 것으로, 락(잠금)을 이용해 구현한다. 한 스레드가 락을 가진 동안 다른 스레드는 기다린다.
- synchronized
- 자바에서 임계 영역을 지정하는 키워드다. synchronized가 붙은 메서드나 블록은 한 번에 한 스레드만 실행할 수 있으며, 실행하려는 스레드는 해당 객체의 모니터 락을 얻어야 한다. 간단하지만 락을 기다리는 스레드는 멈춰 있게 되므로 범위를 필요한 만큼만 잡아야 한다.
- 힙 덤프와 스레드 덤프
- 힙 덤프는 특정 시점에 힙에 있는 모든 객체의 정보를 파일로 저장한 것으로, 어떤 객체가 메모리를 많이 차지하고 무엇이 그것을 참조하는지 분석해 메모리 누수의 원인을 찾는 데 쓴다. 스레드 덤프는 그 시점의 모든 스레드가 무엇을 실행 중인지(스택)를 기록한 것으로, 응답 없음이나 교착 상태의 원인을 찾는 데 쓴다.
이렇게 바꿔 보세요
- 처음 설정으로 — 서로 반대 순서로 락을 잡고 동시에 시작합니다. 교착 상태가 됩니다.
- 락 순서를 통일하면 — 둘 다 A부터 잡습니다. 한쪽이 기다렸다가 차례로 끝납니다.
- 타이밍이 어긋나면 — 반대 순서인데도 문제없이 끝납니다. 그래서 테스트에서 안 잡힙니다.
- 기다리는 시간에 제한을 두면 — 못 잡으면 가진 락을 내려놓고 다시 시도합니다.
처음 설정으로 실행하면
- 스레드 1: 보내는 계좌 A의 락을 잡았습니다.
- 스레드 2: 보내는 계좌 B의 락을 잡았습니다.
- 스레드 1: 받는 계좌 B의 락을 잡으려 합니다. 스레드 2가 쥐고 있어 기다립니다. A의 락은 쥔 채입니다.
- 스레드 2: 받는 계좌 A의 락을 잡으려 합니다. 스레드 1이 쥐고 있어 기다립니다. 스레드 1은 스레드 2를, 스레드 2는 스레드 1을 기다립니다. 어느 쪽도 먼저 락을 내려놓지 않으므로 영원히 풀리지 않습니다. 교착 상태입니다.
실행 결과
- 교착 상태가 됐습니다. 두 이체 모두 끝나지 않고, 두 스레드는 예외도 없이 멈춰 있습니다. 스레드 덤프를 뜨면 서로의 락을 기다리는 BLOCKED 상태의 스레드로 확인할 수 있습니다.
- 교착 상태는 네 조건이 모두 맞을 때 생깁니다: 한 번에 하나만 쓸 수 있는 자원, 가진 채로 다른 것을 기다림, 남의 것을 뺏을 수 없음, 기다림이 고리를 이룸. 이 중 하나만 깨면 됩니다. 락 순서 통일은 고리를 깹니다. tryLock은 끝없이 기다리지 않게 해서, 제한 시간이 지나면 포기하고 쥐고 있던 락을 내려놓게 합니다.
코드
void transfer(Account from, Account to, long amount) {
synchronized (from) { // 보내는 계좌부터 잠근다
synchronized (to) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
// 스레드 1: transfer(A, B, 100)
// 스레드 2: transfer(B, A, 50)
이 모형의 전제
두 스레드가 정확히 엇갈리는 가장 나쁜 순서(각자 첫 번째 락을 잡은 뒤 두 번째 락을 요청)를 가정했습니다. 실제로는 타이밍에 따라 문제없이 지나가는 경우가 더 많고, 그래서 재현이 어렵습니다. synchronized는 기다리는 시간에 제한을 둘 수 없어서, 제한이 필요하면 ReentrantLock의 tryLock을 씁니다. DB에서도 두 트랜잭션이 행을 서로 반대 순서로 잠그면 같은 일이 생기며, DB는 이를 감지해 한쪽을 강제로 롤백시킵니다. 이 PC의 JDK 21에서 실제로 실행해 확인했습니다. 반대 순서로 동시에 잡으면 두 스레드가 BLOCKED 상태로 멈췄고 JVM이 교착 스레드 2개를 감지했습니다. 같은 순서로 잡거나 차례로 실행하면 끝났고, tryLock을 쓰면 재시도 1번 뒤에 둘 다 끝났습니다.
앞 실습: 경쟁 상태 · 다음 실습: 스레드 풀