교착 상태(DeadLock)은 두 개 이상의 프로세스가 서로 자원을 점유한 상태에서, 다른 프로세스가 점유한 자원을 요청하며 무한히 대기하게 되는 상태. 이 상태에서는 더 이상 어떤 프로세스도 작업을 진행할 수 없으므로 시스템 일부 또는 전체가 멈춤.
4가지 필요 조건(Coffman 조건)
1. 상호 배제(Mutual Exclusion) : 자원은 한번에 하나의 프로세스만 사용할 수 있어야 함.
2. 점유 및 대기(Hold and Wait) : 자원을 점유한 프로세스가 다른 자원을 요청하며 대기 상태에 있어야 함.
3. 비전섬(No Preemption) : 프로세스가 점유한 자원을 강제로 빼앗을 수 없음.
4. 순환 대기(Circular Wait) : 프로세스들이 순환 구조로 자원을 요청하며 대기함.
예방 방법
1. 상호 배제 조건 제거 : 자원의 공유를 가능하게 하거나, 공유 자원한 자원을 설계. 하지만 대부분의 자원(프린터, 파일 쓰기 등)은 본질적으로 상호 배제가 필요.
2. 점유 및 대기 조건 제거 : 프로세스가 자원을 요청할 때 이미 모든 자원을 점유한 상태에서 실행을 시작하도록 설계. 자원이 부족할 경우 프로세스가 아무 자원도 점유하지 않고 대기 상태로 들어가도록 설계.
3. 비선점 조건 제거. 한 프로세스가 자원을 점유하고 있는 동안 다른 프로세스가 이를 요청하면 점유 중인 프로세스가 자원을 반환하도록 설계.
4. 순환 대기 조건 제거 : 자원에 번호를 부여하고 항상 낮은 번호에서 높은 번호로만 자원을 요청하도록 규칙을 정함. 이를 통해 순환 구조가 발생하지 않도록 방지.
해결방법
1. 교착 상태 탐지 및 복구(Detection and Recovery) : 교착 상태를 탐지하기 위해 자원 할당 그래프를 사용하거나, 교착 상태 탐지 알고리즘 활용
: 자원 할당 그래프는 프로세스와 자원의 관계를 시각적으로 표현하여 자원의 점유와 요청 상태를 파악할 수 있다. 이를 통해 사이클이 탐지되면 교착 상태임을 알 수 있다.
2. 자원 할당 방지(Banker's Algorithm) : 시스템이 프로세스의 자원 요청을 허용하기 전에 요청을 허용했을 때 안전 상태가 유지되는지 확인
3. 타임아웃(Timeouts) : 프로세스가 일정 시간 동안 자원을 점유하지 못하면 요청을 취소하고 다시 시도하도록 설계.
4. 프로세스 우선순위 조정 : 프로세스에 우선순위를 부여하여 특정 프로세스가 자원을 계속 점유하거나 기다리게 되는 상황을 방지.
교착 상태가 발생하는 코드를 작성하고 이를 예방하거나 해결하는 코드를 작성해 볼 수 있습니다.
public class DeadlockExample {
private final Object resource1 = new Object();
private final Object resource2 = new Object();
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
Thread thread1 = new Thread(() -> example.method1());
Thread thread2 = new Thread(() -> example.method2());
thread1.start();
thread2.start();
}
public void method1() {
synchronized (resource1) {
System.out.println("Thread 1: Locked resource 1");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1: Locked resource 2");
}
}
}
public void method2() {
synchronized (resource2) {
System.out.println("Thread 2: Locked resource 2");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread 2: Locked resource 1");
}
}
}
}
Thread 1과 Thread 2가 각각 자원 resource1과 resource2를 점유한 후 서로 상대방의 자원을 요청하며 교착 상태에 빠집니다.위 코드에서 자원 요청 순서를 정해 순환 대기 조건을 제거해 봅니다.
public void method1() {
synchronized (resource1) {
System.out.println("Thread 1: Locked resource 1");
synchronized (resource2) {
System.out.println("Thread 1: Locked resource 2");
}
}
}
public void method2() {
synchronized (resource1) { // resource1을 먼저 점유하도록 변경
System.out.println("Thread 2: Locked resource 1");
synchronized (resource2) {
System.out.println("Thread 2: Locked resource 2");
}
}
}
Spring에서 트랜잭션(Transaction) 관리 중 잘못된 자원 잠금으로 교착 상태가 발생할 수 있습니다. 이를 해결하기 위한 방법을 연습할 수 있습니다.
@Transactional 어노테이션을 사용합니다.@Transactional(isolation = Isolation.SERIALIZABLE)
public void performTransaction() {
// 데이터베이스 작업 수행
}
@Lock과 같은 JPA 기능을 사용해 동시 자원 접근을 제한합니다.@Lock(LockModeType.PESSIMISTIC_WRITE)
public Optional<Entity> findByIdWithLock(Long id) {
return repository.findById(id);
}
Java에서 ThreadMXBean을 사용해 교착 상태를 탐지하는 코드 작성.
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadlockDetection {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
System.out.println("Deadlocked threads detected:");
for (ThreadInfo info : threadInfos) {
System.out.println(info.getThreadName());
}
} else {
System.out.println("No deadlocked threads detected.");
}
}
}
synchronized, Lock 등)을 활용.위 실습을 통해 멀티스레드 환경에서의 교착 상태를 실질적으로 이해하고, Spring 기반 백엔드 개발에서도 발생할 수 있는 문제를 예방 및 해결하는 경험을 쌓을 수 있습니다.