Icednut's Note

Deadlock이 뭐지? (Java Thread와 Deadlock에 대한 고찰)

2016-08-06

들어가기 전에

최근에 Deadlock이 뭔지 고민해보는 시간이 있었다. 뭐 요즘 들어서 Reactor다 Akka다 뭐다 해서 비동기 프로그래밍을 손쉽게 할 수 있는 프레임워크나 라이브러리가 있어서 Deadlock에 대해 신경을 안쓰고 살고 있어 대답이 선뜻 나오질 못했다. Deadlock이 뭔가요에 대한 질문에 그냥 막연히 다수의 Thread가 서로의 Lock을 기다리는 상황이라고만 대답했는데, 이참에 좀 더 구체적으로 Deadlock이 뭐고 원인, 해결에는 뭐가 있는지 알아봐야 겠다. 정리 하는김에 Java Fork & Join, ThreadLocal, stream parallel에 대해서도 살펴봐야겠다.

Deadlock의 원인

현실에서 Deadlock 상황

데드락은 예전부터 식사하는 철학자 dining philosophers 문제로 널리 알려져 왔다. 다섯 명의 철학자가 중국 음식점에 저녁 식사를 하러 가서 둥그런 테이블에 앉았다. 테이블에는 다섯 개의 젓가락(다섯 쌍이 아닌 다섯 개)이 개인별 접시 사이에 하나씩 놓여있다. 철학자는 ‘먹는’ 동작과 ‘생각하는’ 동작을 차례대로 반복한다. 먹는 동안에는 접시 양쪽에 있는 젓가락 두 개를 모아 한 쌍을 만들어야 자신의 접시에 놓인 음식을 먹을 수 있고, 음식을 먹은 이후에는 젓가락을 다시 양쪽에 하나씩 내려 놓고 생각을 시작한다.

(중략)

모든 철학자가 각자 자기 왼쪽에 있는 젓가락을 집은 다음 오른쪽 젓가락을 사용할 수 있을 때까지 기다렸다가 오른쪽 젓가락을 집어서 식사를 한다면, 모든 철학자가 더 이상 먹지 못하는 상황에 다다를 수 있다. 철학자 모두가 먹지 못하는 상황은 음식을 먹는 데 필요한 자원을 모두 다른 곳에서 확보하고 놓지 않기 때문에 모두가 서로 상대방이 자원을 놓기만을 기다리는, 이른바 데드락이 걸린다.
자바 병렬 프로그래밍 P.305

식사하는 철학자의 문제를 Java Thread에 접목하여 Deadlock이 생기는 과정을 살펴보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 데드락 위험이 있는 코드
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
synchronized (left) {
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
synchronized (right) {
synchronized (left) {
doSomethingElse();
}
}
}
}
  • 스레드 A가 락 left을 확보한 상태에서 락 right을 확보하려 대기
  • 스레드 B가 락 right을 확보한 상태에서 락 left을 확보하려고 대기
  • 양쪽 스레드 A, B는 서로가 락을 풀기를 영원히 기다리게 됨

위와 같이 Java Thread에서도 스레드 하나가 특정 락(Lock)을 놓지 않고 계속 잡고 있으면 그 락을 확보하려는 다른 스레드는 락이 풀릴 때까지 기다리는 수 밖에 없다. Deadlock은 Thread가 두 개의 락을 획득하려 하는 코드에서 나타난다. 학교에서 배울 때는 이 정도 수준에서 멈추는 경우가 많은데 데드락은 상용 서비스를 시작하고 나서 시스템에 부하가 걸리는 경우와 같이 최악의 상황에서 그 모습을 드러내곤 한다. 더군다나 아주 심도있는 방법으로 부하 테스트(load-testing)을 진행했다 하더라도 발생 가능한 데드락 모두 찾아낼 수는 없다. JVM에서는 데이터베이스 서버와 같이 데드락 상태 추적 기능이 없기 때문에 Java Application에서 데드락이 발생했을 때 정상으로 되돌리려 한다면 애플리케이션을 종료하고 다시 실행하는 것밖에 없다.

Deadlock 예방하기

방법 1. Lock이 발생하는 순서를 정해놓는다.

프로그램 내부의 모든 스레드에서 필요한 락을 모두 같은 순서로만 사용한다면, 락 순서에 의한 데드락은 발생하지 않는다.
자바 병렬 프로그래밍 P.307

1
2
3
4
5
6
7
8
9
10
11
12
13
// 해결 전: 데드락 위험이 있는 코드
public void transferMoney (Account fromAccount, Account toAccount, DollarAmount amount) {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}

위 코드를 얼핏보면 경합이 일어나지 않을 코드 같다. 하지만 파라미터 fromAccount와 toAccount에 순서만 달리해서 동시 호출이 일어난다면 데드락이 걸릴 확률이 증가하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 해결 후: Lock이 발생하는 순서를 제어한 경우
private static final Object tieLock = new Object();
public void transferMoney(final Account fromAccount, final Account toAccount, final DollarAmount amount) {
class Helper {
public void transfer() {
if (fromAccount.getBalance().compareTo(acmount) < 0) {
throw new InsufficientFuncsException();
} else {
fromAccount.debit(amount);
toAccount.credit(ammount);
}
}
}
int fromHash = System.identityHashCode(fromAccount);
int toHash = System.identityHashCode(toAccount);
if (fromHash < toHash) {
synchronized(fromAccount) {
synchronized(toAccount) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized(toAccount) {
synchronized(fromAccount) {
new Helper().transfer();
}
}
} else {
synchronized (tieLock) {
synchronized(fromAccount) {
synchronized(toAccount) {
new Helper().transfer();
}
}
}
}
}

방법 2. 오픈 호출

메소드 호출이라는 것은 그 너머에 어떤 일이 일어나는지 모르게 막아주는 추상화 방법이다. 하지만 호출한 메소드 내부에서 어떤 일이 일어나는지 알지 못하기 때문에 특정 락을 확보한 상태에서 다른 메소드를 호출한다는 것은 파급 효과를 분석하기가 어렵고 위험한 일이다. 이에 따라 락을 전혀 확보하지 않은 상태에서 메소드를 호출하는 것이 좋은데 이것을 오픈 호출이라고 한다. (스레드 안정성을 확보하기 위해 캡슐화 기법encapsulation을 사용하는 것과 비슷)

락을 확보하지 않은 상태에서 메소드를 호출하는게 관건!

방법 3. 락의 시간 제한

암묵적인 락 synchronized 말고 락 시간을 제한할 있는 Lock 클래스의 tryLock 메소드를 사용한다. 암묵적인 락은 락을 확보할 때까지 영원히 기다리지만, Lock 클래스 등의 명시적인 락은 일정 시간을 정해두고 그 시간 동안 락을 확보하지 못한다면 tryLock 메소드가 오류를 발생시키도록 할 수 있다.

스레드 덤프를 활용한 Deadlock 분석하기

스레드 덤프 분석이라면 여기 훌륭한 글이 이미 있다.
http://d2.naver.com/helloworld/10963

Tags: java Thread Deadlock