1. Thread Dump와 스레드 상태 분석
Thread Dump(스레드 덤프)는 특정 시점에 애플리케이션의 모든 스레드 상태를 찍은 스냅샷입니다. 모든 스레드의 단체 사진과 같습니다: 누가 무엇을 하고 있는지, 어디서 막혔는지, 누구를 기다리는지. Thread Dump는 deadlock, livelock 등의 수수께끼 같은 멈춤을 찾는 데 가장 중요한 도구입니다.
Thread Dump는 어떻게 얻나요?
터미널을 통해 (jstack):
Java 프로세스의 PID가 있다면 다음을 실행하세요:
jstack <PID>
이 명령은 모든 스레드의 상태와 각 스레드가 어떤 상태인지, 어떤 모니터(락)를 보유 중인지 콘솔에 출력합니다.
IDE(IntelliJ IDEA)에서:
메뉴 "Run" → "Show Running List" → 프로세스 선택 → "Thread Dump".
VisualVM 또는 JConsole에서:
프로세스를 열고 "Threads" 탭을 찾아 상태를 스냅샷으로 저장합니다.
Thread Dump 예시
덤프의 일부:
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e0c7800 nid=0x1a48 waiting for monitor entry [0x000000001f00f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
- waiting to lock <0x00000000d6d6baf8> (a java.lang.Object)
- locked <0x00000000d6d6bb08> (a java.lang.Object)
여기서 "Thread-1" 스레드는 BLOCKED 상태이며, 한 모니터는 보유하고 있지만 다른 모니터를 기다리고 있음을 볼 수 있습니다. A를 보유하고 B를 기다리는 스레드와, B를 보유하고 A를 기다리는 스레드가 함께 보이면 전형적인 데드락입니다.
스레드 상태
| 상태 | 설명 |
|---|---|
| RUNNABLE | 스레드가 실행 중이거나 실행할 준비가 됨 |
| BLOCKED | 모니터(락) 획득을 기다림 |
| WAITING | notify()/notifyAll()을 기다림(예: wait() 호출) |
| TIMED_WAITING | 타임아웃과 함께 대기(예: sleep, wait(timeout)) |
| TERMINATED | 스레드가 종료됨 |
중요: RUNNABLE 상태라고 해서 스레드가 지금 당장 실행 중이라는 뜻은 아닙니다 — 단지 실행 준비가 되었음을 의미합니다(플래너인 JVM이 즉시 실행하지 않을 수 있습니다).
데드락을 어떻게 알아차리나요?
덤프에서 여러 스레드가 BLOCKED 상태이며, 각 스레드가 같은 집합의 다른 스레드가 보유한 모니터를 기다리고 있다면 데드락입니다.
덤프의 끝부분에 jstack은 보통 다음과 같이 표시합니다:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000d6d6baf8 (object 0x00000000d6d6baf8, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00000000d6d6bb08 (object 0x00000000d6d6bb08, a java.lang.Object),
which is held by "Thread-1"
스레드가 오랫동안 BLOCKED 또는 WAITING 상태에 머문다면 조사할 충분한 근거입니다.
2. 스레드 모니터링과 프로파일링
VisualVM
VisualVM은 대부분의 JDK에 포함된 무료 유틸리티입니다. 프로세스에 연결하여 스레드 상태를 확인하고 Thread Dump를 찍으며 CPU 부하, 활성 스레드와 "멈춰 있는" 스레드를 볼 수 있습니다.
Threads 탭: 생성된 스레드 수, 상태, 활동 내역을 볼 수 있습니다.
Thread Dump: "Thread Dump" 버튼으로 jstack과 유사한 스냅샷을 찍습니다.
Java Mission Control과 Flight Recorder
Java Mission Control (JMC): JVM 동작을 실시간으로 분석하는 고급 도구입니다. 락, 실행 시간, 할당, 지연 등을 조사하는 데 도움이 됩니다.
Java Flight Recorder (JFR): JVM에 내장된 프로파일러로, 스레드, 락, 중단 시간 등과 관련된 이벤트를 수집합니다.
예: 락 모니터링
VisualVM 또는 JMC에서 다음과 같은 상황을 확인할 수 있습니다:
- 스레드 "A"는 객체 X에서 블록됨.
- 스레드 "B"는 객체 X를 보유하지만 객체 Y를 기다림.
- 스레드 "C"는 객체 Y를 보유하지만 객체 X를 기다림.
이는 전형적인 순환 락(데드락)입니다.
실무에서 이 도구들을 어떻게 사용하나요?
- 애플리케이션을 -XX:+FlightRecorder 옵션으로 실행하세요(또는 JDK 11+를 사용하세요).
- JMC를 열고 프로세스에 연결한 뒤 기록을 시작하세요(start recording).
- 핫스팟, 장시간 락, 스레드 간 경쟁을 분석하세요.
3. 로깅과 트레이싱
멀티스레드 프로그램에서 눈대중 디버깅은 고통을 부릅니다. 임계 구역(synchronized 블록) 출입, 공유 변수 작업, 스레드의 대기와 깨움 이벤트를 로깅하세요 — 누가 언제 리소스를 획득하거나 해제했는지 파악할 수 있습니다.
어떻게 로깅할까요?
- 표준 도구를 사용하세요: java.util.logging, SLF4J, Log4j.
- 스레드 이름을 로깅하세요: Thread.currentThread().getName().
- 시간과 스레드 식별자를 로깅하세요.
- 락 획득/해제 이벤트를 로깅하세요.
로깅 예시
synchronized(lock) {
System.out.println(Thread.currentThread().getName() + " lock을 획득함");
// 임계 구역
System.out.println(Thread.currentThread().getName() + " lock을 빠져나감");
}
스레드 이름 사용
스레드에 의미 있는 이름을 부여하세요!
Thread t = new Thread(runnable, "MyWorker-1");
로거를 이용한 트레이싱 예시
import java.util.logging.Logger;
public class Example {
private static final Logger logger = Logger.getLogger(Example.class.getName());
public void doWork() {
logger.info(Thread.currentThread().getName() + " 작업을 시작함");
synchronized (this) {
logger.info(Thread.currentThread().getName() + " synchronized에 진입함");
// ...
}
logger.info(Thread.currentThread().getName() + " 작업을 종료함");
}
}
4. 진단 모범 사례
락 범위를 최소화하세요
락을 가능한 짧은 시간만 보유하세요.
나쁜 예:
synchronized(lock) {
// 오래 걸리는 I/O
// 복잡한 계산
// DB 접근
// ... 그리고 마지막에야 공유 데이터 작업
}
좋은 예:
// synchronized 바깥에서: 오래 걸리는 I/O, 계산
synchronized(lock) {
// 공유 데이터 작업만
}
스레드 이름을 사용하세요
의미 있는 스레드 이름은 덤프와 로그 분석 시간을 줄여줍니다.
멀티스레드 테스트를 작성하세요
경쟁 시나리오를 모의하기 위해 JUnit + CountDownLatch를 사용하세요.
CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
// ...
latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // 두 스레드가 끝날 때까지 대기
try-finally를 ReentrantLock에 사용하세요
Lock lock = new ReentrantLock();
lock.lock();
try {
// 임계 구역
} finally {
lock.unlock();
}
이렇게 하면 예외가 발생해도 락 해제를 잊지 않습니다. 상호 교착을 피하기 위해서는 tryLock()에 타임아웃을 사용하세요.
왜 동기화가 필요한지 문서화하세요
"여기서는 synchronized가 필요한 이유는 …" 같은 주석은 시간이 지나도 의도를 이해하는 데 도움이 됩니다.
5. 실습: 테스트 프로그램에서 데드락 분석
데드락이 있는 코드 예
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1: lockA를 획득함");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockB) {
System.out.println("Thread-1: lockB를 획득함");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2: lockB를 획득함");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockA) {
System.out.println("Thread-2: lockA를 획득함");
}
}
}, "Thread-2");
t1.start();
t2.start();
}
}
데드락을 어떻게 잡을까
- 프로그램을 실행하면 — 멈춥니다.
- thread dump를 얻습니다 (jstack 또는 VisualVM).
- "Thread-1"과 "Thread-2"를 찾아보면 — 각각 하나의 lock을 잡고 다른 lock을 기다리고 있음을 볼 수 있습니다.
- 덤프 끝부분에는 "Found one Java-level deadlock" 섹션이 나타납니다.
해결 방법
- 항상 동일한 순서로 락을 획득하세요.
- ReentrantLock의 tryLock()과 타임아웃을 사용하세요: 모든 락을 획득하지 못하면 — 보유 중인 락을 해제하고 다시 시도합니다.
6. 멀티스레드 진단 시 흔한 실수
오류 №1: thread dump를 읽지 못함. 초보 개발자는 덤프를 무서워합니다: "이 이상한 스택 트레이스와 상태는 뭐지?" 실제로는 기본 상태만 알고 BLOCKED/WAITING을 찾아보면 분석이 훨씬 쉬워집니다.
오류 №2: 스레드 이름을 무시함. 의미 있는 이름이 없으면 덤프를 파악하는 건 건초더미에서 바늘 찾기와 같습니다. 이름을 꼭 지정하세요!
오류 №3: 지나치게 큰 synchronized 블록. 큰 코드 덩어리를 동기화하면 스레드가 더 자주 서로를 블록하게 됩니다 — 덤프의 잦은 BLOCKED로 확인할 수 있습니다.
오류 №4: RUNNABLE과 실제 실행 중인 스레드를 혼동함. RUNNABLE은 항상 CPU에서 "달리고" 있음을 뜻하지 않습니다. JVM 스케줄러가 누구를 실행할지 결정합니다.
오류 №5: 모니터링 도구를 사용하지 않음. 많은 이들이 VisualVM, JMC, Flight Recorder를 모르고 println만으로 고생합니다. 도구들을 활용하면 삶이 훨씬 쉬워집니다.
오류 №6: 핵심 작업 로깅 부재. 누가 언제 락을 획득/해제했는지 로그 없이 파악하기는 거의 불가능합니다.
오류 №7: "감"으로 데이터 레이스를 잡으려 함. 레이스는 항상 즉시 드러나지 않습니다 — CountDownLatch로 테스트를 만들고 Thread.yield()로 경쟁을 유도하며 공유 변수 상태를 분석하세요.
GO TO FULL VERSION