교착 상태
교착 상태(Deadlock)는 두 개 이상의 프로세스가 서로 상대방이 보유한 자원을 기다리며 영원히 진행되지 못하는 상태입니다. OS는 교착 상태를 예방·회피·탐지·복구 같은 전략으로 다루며, 교재에 따라 무시하는 방식까지 함께 분류합니다.
핵심 개념
교착 상태 4조건 (코프만)
상호 배제(Mutual Exclusion) · 점유와 대기(Hold and Wait) · 비선점(No Preemption) · 순환 대기(Circular Wait) — 네 조건이 동시에 성립할 때만 교착 상태가 발생합니다. 하나만 깨도 예방됩니다.
핵심 개념
자원 할당 그래프 (RAG)
프로세스(원)와 자원(사각형)의 관계를 방향 그래프로 표현합니다. 요청 간선: 프로세스→자원. 할당 간선: 자원→프로세스. 그래프에 사이클이 존재하면 교착 상태이거나 교착 상태 가능성이 있습니다(자원 종류마다 인스턴스가 하나이면 사이클이 곧 교착 상태이고, 여러 개이면 사이클은 필요조건일 뿐입니다).
핵심 개념
교착 상태 예방
4조건 중 하나를 원천 차단합니다. 순환 대기 예방이 실무에서 가장 흔합니다 — Lock 획득 순서를 전역적으로 통일합니다. 비선점 허용(우선순위 기반 선점)이나 점유와 대기 금지(모든 자원 한꺼번에 요청)도 방법입니다.
핵심 개념
은행원 알고리즘 (회피)
자원을 할당하기 전에 "안전 상태"인지 시뮬레이션합니다. 모든 프로세스가 순차적으로 완료될 수 있는 안전 순서가 존재하면 할당합니다. 최대 자원 요구량을 미리 선언해야 하는 단점이 있습니다.
핵심 개념
교착 상태 탐지 · 복구
주기적으로 자원 할당 그래프의 사이클을 탐지합니다. 복구 방법: 프로세스 종료(하나씩 또는 전체), 자원 선점(특정 프로세스에서 자원 강제 회수 후 롤백). 교착 상태가 드물다고 보고 대처하지 않는 방식을 Tanenbaum 교재는 타조 알고리즘(Ostrich Algorithm)이라 부릅니다.
핵심 개념
라이브락 · 기아
라이브락(Livelock)은 프로세스가 실행은 되지만 상태가 계속 바뀌면서 진행되지 못하는 상태입니다. 기아(Starvation)는 낮은 우선순위 프로세스가 계속 자원을 할당받지 못합니다. 둘 다 교착 상태는 아니지만 유사한 문제입니다.
교착 상태는 어떻게 생기나
락이나 자원을 하나 잡은 채 다른 것을 기다리는데, 서로가 상대가 가진 것을 기다리면 누구도 진행하지 못합니다. 교재는 상호 배제, 점유와 대기, 비선점, 순환 대기의 네 가지 조건이 모두 성립할 때 교착 상태가 생긴다고 설명하며, 이를 Coffman 조건이라고도 부릅니다(관련 문헌: Coffman 등, 'System Deadlocks', ACM Computing Surveys, 1971).
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
교착 상태의 발생 조건 네 가지는 무엇인가요?
상호 배제, 점유와 대기, 비선점, 순환 대기입니다. 네 조건이 동시에 성립할 때 발생하므로 하나를 깨면 예방됩니다.
면접 질문
예방, 회피, 탐지와 복구는 어떻게 다른가요?
예방은 네 조건 중 하나를 구조적으로 막는 것이고, 회피는 자원을 줄 때마다 안전한지 확인하는 것(은행원 알고리즘)입니다. 탐지와 복구는 일단 발생을 허용하고 사이클을 찾아 프로세스를 종료하거나 자원을 회수합니다.
면접 질문
실무에서 교착 상태를 어떻게 피하나요?
락을 잡는 순서를 전역으로 통일하고, 락을 잡은 채 오래 걸리는 작업이나 다른 락 요청을 피하며, tryLock에 타임아웃을 둡니다. 트랜잭션도 같은 순서로 자원에 접근하게 합니다.
면접 질문
교착 상태가 생겼는지 어떻게 알아내나요?
Java는 ThreadMXBean으로 교착 상태의 플랫폼 스레드를 찾을 수 있고, 데이터베이스는 대기 사슬을 탐지해 한 트랜잭션을 중단시킵니다.
실제 시스템에서는
deadlock_timeout이며 기본값이 1초입니다.
ThreadMXBean.findDeadlockedThreads()는 락(object monitor, ownable synchronizer) 때문에 교착 상태에 빠진 플랫폼 스레드를 찾는 메서드이고, 가상 스레드는 대상이 아니라고 Javadoc이 설명합니다.
함께 보면 좋은 토픽
참고 자료
C — 교착 상태 발생 / Lock 순서 통일로 예방
#include <stdio.h> #include <pthread.h> pthread_mutex_t r1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t r2 = PTHREAD_MUTEX_INITIALIZER; /* ── 교착 상태 발생 버전 ─────────────────────────────────── */ void *thread1_bad(void *arg) { pthread_mutex_lock(&r1); // R1 획득 pthread_mutex_lock(&r2); // R2 요청 → T2가 보유 시 블로킹 /* 임계 구역 */ pthread_mutex_unlock(&r2); pthread_mutex_unlock(&r1); return NULL; } void *thread2_bad(void *arg) { pthread_mutex_lock(&r2); // R2 획득 pthread_mutex_lock(&r1); // R1 요청 → T1이 보유 시 블로킹 → 교착! /* 임계 구역 */ pthread_mutex_unlock(&r1); pthread_mutex_unlock(&r2); return NULL; } /* ── 예방 버전: 항상 r1 → r2 순서로 획득 ───────────────────── */ void *thread1_safe(void *arg) { pthread_mutex_lock(&r1); // 낮은 번호(r1) 먼저 pthread_mutex_lock(&r2); // 높은 번호(r2) 나중 /* 임계 구역 — 순환 대기 불가 */ pthread_mutex_unlock(&r2); pthread_mutex_unlock(&r1); return NULL; } void *thread2_safe(void *arg) { pthread_mutex_lock(&r1); // T1·T2 모두 r1 먼저 → 순환 없음 pthread_mutex_lock(&r2); /* 임계 구역 */ pthread_mutex_unlock(&r2); pthread_mutex_unlock(&r1); return NULL; } int main() { pthread_t t1, t2; /* safe 버전 실행 */ pthread_create(&t1, NULL, thread1_safe, NULL); pthread_create(&t2, NULL, thread2_safe, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("완료 — 교착 상태 없음\n"); return 0; }
교착 상태는 Lock 획득 순서를 정반대로 할 때 발생합니다. T1이 r1→r2, T2가 r2→r1 순서로 획득하면 각자 첫 번째 Lock을 보유한 채 두 번째 Lock을 기다리는 순환 대기가 형성됩니다.
예방책: 모든 스레드에서 Lock 번호 오름차순(r1 → r2)으로만 획득합니다. 이렇게 하면 순환 대기 조건이 성립할 수 없어 교착 상태가 발생하지 않습니다.
pthread_mutex_trylock()은 Lock이 이미 잠겨 있으면 즉시 EBUSY로 실패하는 비블로킹(non-blocking) 함수입니다. 시간 제한 대기가 필요하면 pthread_mutex_timedlock()을 사용합니다. trylock 실패 시 보유한 Lock을 풀고 재시도하는 방식으로 교착을 회피할 수 있지만, 라이브락 가능성이 있으므로 재시도 간격에 지수 백오프(Exponential Backoff)를 적용해야 합니다.
Java — 교착 상태 + tryLock 타임아웃으로 회피
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class DeadlockDemo { private static final ReentrantLock r1 = new ReentrantLock(); private static final ReentrantLock r2 = new ReentrantLock(); /* ── 교착 상태 발생 가능 버전 ──────────────────────────────── */ static void badTask1() throws InterruptedException { r1.lock(); Thread.sleep(50); // T2가 r2를 잡을 시간 부여 r2.lock(); // T2가 r2 보유 시 블로킹 → 교착 위험 try { /* 작업 */ } finally { r2.unlock(); r1.unlock(); } } /* ── tryLock 타임아웃으로 회피 ─────────────────────────────── */ static boolean safeTask(ReentrantLock first, ReentrantLock second) throws InterruptedException { if (!first.tryLock(100, TimeUnit.MILLISECONDS)) { return false; // 100ms 내 획득 실패 → 포기 } try { if (!second.tryLock(100, TimeUnit.MILLISECONDS)) { return false; // 두 번째 획득 실패 → first도 자동 해제(finally) } try { /* 두 Lock 모두 보유 — 안전하게 작업 */ System.out.println("작업 완료"); return true; } finally { second.unlock(); } } finally { first.unlock(); } } public static void main(String[] args) throws Exception { Thread t1 = new Thread(() -> { try { while (!safeTask(r1, r2)) { Thread.sleep(10 + (long)(Math.random()*40)); // 지수 백오프 대체 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread t2 = new Thread(() -> { try { // 동일 순서(r1→r2)로 요청 — 순환 대기 예방 while (!safeTask(r1, r2)) { Thread.sleep(10 + (long)(Math.random()*40)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); t1.start(); t2.start(); t1.join(); t2.join(); } }
ReentrantLock.tryLock(timeout)은 지정 시간 내에 Lock을 획득하지 못하면 false를 반환합니다. 두 번째 Lock 획득에 실패하면 첫 번째 Lock도 finally 블록에서 즉시 해제되어 교착 상태를 회피합니다.
tryLock 방식은 라이브락 가능성이 있으므로 재시도 간격에 랜덤 지연(Jitter)을 추가해야 합니다. 두 스레드가 정확히 같은 간격으로 재시도하면 계속 충돌할 수 있습니다.
가장 안전한 방법은 Lock 획득 순서를 코드 레벨에서 강제(항상 낮은 번호 → 높은 번호)하는 것입니다.
Java의 System.identityHashCode()로 객체 ID 기준 정렬 후 획득하는 패턴이 실무에서 사용됩니다.
Q1
교착 상태(Deadlock)가 발생하기 위한 4가지 필요조건에 포함되지 않는 것은?
Q2
자원 할당 그래프(RAG)에서 사이클이 존재한다는 것은?
Q3
데드락을 예방하는 방법으로 '점유와 대기' 조건을 깨는 방법은?