본문 바로가기

java

[Java] Thread Dump

Thread Dump란?
문제상황1. 데드락
문제상황2. 처리 지연

 

Thread Dump란?

실행 중인 서버의 thread 상태의 snapshot을 획득하는 행위를 말합니다. 앞선 글 동시성 처리 - intro에서 살펴보았듯이 멀티스레드 환경에서 발생하는 여러 이슈들이 있습니다. 각 스레드가 어떤 상태에 놓여 있는지 thread dump를 활용하여 분석하면 어디서 문제가 발생하였는지 측정하여 이를 해결할 수 있습니다.

이번 글에서는 VisualVM을 활용해 특정 문제 상황들을 재현한 뒤 thread의 상태를 분석해보도록 하겠습니다. NaverD2 - 스레드 덤프 분석하기를 보면 jstack, JMC 등을 이용해  thread dump를 뜨면 로그 형태의 text로 각 thread의 정보를 확인할 수 있는데, VisualVM을 활용하면 이를 가시적으로 확인할 수 있습니다.

이번 글에서는 데드락, 처리 지연, 스레드풀 기아(starvation) 상황을 살펴보도록 하겠습니다.

 

문제상황1. 데드락

고유 락 1과 2를 갖고 있는 Deadlock 클래스를 생성합니다. lock 1 -> 2 순서로 락을 획득하는 acquireLock12() 메서드와, lock 2 -> 1 순서로 락을 획득하는 acquireLock21() 메서드를 선언했습니다. 각 메서드는 각기 다른 스레드에서 호출할 것입니다. 이 때, acquireLock12() 메서드는 lock1을 획득하고 5초 뒤에 lock2를 획득하는 시도를 합니다. acquireLock21() 메서드는 lock2를 획득하고 3초 뒤에 lock1을 획득하는 시도를 합니다. 각 스레드가 다른 스레드에서 점유하고 있는 락을 획득하기 위해 대기해야 하므로 데드락이 발생할 것으로 예상할 수 있습니다.

public class Deadlock {

    Object lock1 = new Object();
    Object lock2 = new Object();

    public void acquireLock12() throws InterruptedException {
        synchronized (lock1) {
            Thread.sleep(5000);
            acquireLock21();
        }
    }

    public void acquireLock21() throws InterruptedException {
        synchronized (lock2) {
            Thread.sleep(3000);
            acquireLock12();
        }
    }
}

main 메서드를 통해 각 메서드를 각기 다른 스레드를 통해 실행시켜보겠습니다. main 메서드가 종료되지 않기 위해 Thread를 sleep 하는 코드를 추가하였습니다.

public class DeadlockApp {

    private static final Deadlock deadlock = new Deadlock();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> acquireLock12()).start();
        new Thread(() -> acquireLock21()).start();

        while (true) {
            Thread.sleep(2000);
            System.out.println("waiting...");
        }
    }

    private static void acquireLock12() {
        try {
            deadlock.acquireLock12();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void acquireLock21() {
        try {
            deadlock.acquireLock21();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

이를 VisualVM에서 확인하면 다음과 같이 'Deadlock detected!' 메시지를 확인할 수 있습니다.

 

이제 어떤 스레드로 인해 데드락이 발생했는지 thread dump를 통해 확인해보겠습니다.

Thread-0은 Thread-1에 의해, Thread-1은 Thread-0에 의해 점유되고 있는 락을 획득하기 위해 기다리고 있는 것을 확인할 수 있습니다. 문제가 발생한 지점의 method stack 정보도 함께 제공하고 있습니다.

 

문제상황2. 처리 지연

Thread-1은 fast() 메서드를 호출해 빠르게 종료되도록 하고, Thread-0은 sleep하여 처리에 총 10초가 걸리는 slow() 메서드를 호출하게 하였습니다.

public class SlowApp {

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> callSlow()).start();
        new Thread(() -> callFast()).start();
    }

    private static void callSlow() {
        for (int i = 0; i < 10; i++) {
            try {
                Slow.slow();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void callFast() {
        Fast.fast();
    }
}

class Slow {
    static void slow() throws InterruptedException {
        Thread.sleep(1000);
    }
}

class Fast {
    static void fast() {}
}

 

 

VisualVM의 Thread dump를 통해 확인해보면 다음과 같이 thread의 상태를 볼 수 있습니다. Thread.sleep()을 호출하였기 때문에 dump 시점의 Thread는 cpu 점유시간(cpu=0.52ms) 자체는 적지만 상태는 'TIMED_WATING (sleeping)'으로, 스레드가 호출 후 경과시간(elapsed=7.22s)은 긴 것을 확인할 수 있습니다.

TIMED_WAITING state로 인해 처리가 지연되지 않고, 호출 메서드 자체의 처리가 지연되는 현상을 보기 위해 Long 타입의 number를 10억번 증가시키는 부하를 주도록 하겠습니다. 이 처리는 1~2초의 시간이 소요될 것입니다.

public class SlowApp {

    public static void main(String[] args) {
        new Thread(() -> callSlow()).start();
        new Thread(() -> callFast()).start();
    }

    private static void callSlow() {
        for (int i = 0; i < 10; i++) {
            Slow.slow();
        }
    }

    private static void callFast() {
        Fast.fast();
    }
}

class Slow {
    static void slow(){
        Long number = 0L;
        for (int i = 0; i < 1000000000; i++) {
            number += 1;
        }
    }
}

class Fast {
    static void fast() {}
}

VisualVM의 Thread dump를 통해 확인해보면 다음과 같이 thread의 상태를 볼 수 있습니다. Thread의 state 자체는 RUNNABLE로 실행중이지만, cpu 점유시간(cpu=14981.99ms)은 약 15초로 상대적으로 매우 긴 처리시간을 갖고 있는 것을 확인할 수 있습니다.

'java' 카테고리의 다른 글

[Java] Proxy란?  (0) 2023.03.20
[Java] JNI(Java Native Interface)란?  (0) 2023.03.06
[Java] 테스팅 툴 - JMeter  (1) 2023.02.21
[Java] 모니터링 툴 - VisualVM  (0) 2023.02.19
[Java] 동시성 처리 - Lock Algorithms(락 알고리즘)  (0) 2023.02.19