통신 상태를 확인하기 위한 명령어, 서버 상태를 확인하기 위한 명령어에 이어서 마지막으로 애플리케이션 진단을 위한 명령어를 알아본다. Java 웹 애플리케이션이 비정상적으로 느리게 동작하는 경우, 스레드 덤프를 분석해 봐야 된다.

스레드 덤프를 분석하는 이유

웹 애플리케이션에는 동시에 수 많은 사용자의 요청을 처리하기 위해서 여러 개의 스레드를 사용한다. 이렇게 동시에 여러 개의 스레드가 동작하면 경합 상태가 발생할 것이며, 데드락도 발생할 가능성이 높다.

  • 경합 상태: 여러 개의 스레드가 공유 자원을 동시에 사용하는 상황
  • 데드락: 경합 상태일 때 스레드들이 서로 작업이 끝나기를 기다리고 있어, 작업이 멈추어 버리는 상황

Java 스레드

스레드 동기화

Java에서는 경합 상태일 때 데이터의 정합성을 보장하기 위해 스레드 동기화로 한 번에 하나의 스레드만 공유 자원에 접근할 수도록 한다. 이를 구현하기위해 Java에서는 각 인스턴스들이 자신의 Monitor를 가지게하고, 스레드가 이 인스턴스를 사용할 때 Monitor를 점유하도록 한다. 이 Monitor는 하나의 스레드만 점유할 수 있기때문에, 다른 스레드들은 Wait Queue에서 기다리게 된다.

스레드 상태

Java에서는 스레드의 상태를 Enum으로 선언되어있다. 링크

java-thread-state

  • NEW: 아직 시작되지 않은 스레드 상태
  • RUNNABLE: 현재 JVM 상에서 실행 중인 스레드 상태
  • BLOCKED: 현재 block의 획득을 기다리면는 스레드 상태
  • WAITING: 다른 스레드가 특정 작업을 수행할 때까지 무기한 대기 중인 스레드 상태
  • TIMED_WATING: 다른 스레드가 특정 작업을 수행할 때까지 시간 제한을 걸어두고 대기 중인 스레드 상태
  • TERMINATED: 종료된 스레드 상태

스레드 종류

Java 스레드에는 ‘데몬 스레드(daemon thread)’와 ‘일반 스레드(non-daemon thread)’가 있다.

  • 데몬 스레드: 백그라운드로 실행되고, 일반 스레드가 모두 종료되면 데몬 스레드의 의미가 사라지기 때문에 강제적으로 자동 종료된다. 대표적으로 가비지 컬렉션, 화면 자동갱신 등이 있다.
  • 일반 스레드: 사용자가 직접 생성한 스레드로 대표적으로 `static void main(String[] args)’가 실행되는 스레드가 일반 스레드다.

스레드 우선순위

Java는 스레드 스케줄링에 우선순위 방식과 라운드 로빈 방식을 사용한다. 즉, 우선순위가 높은 스레드가 더 많은 실행을 할 수 있도록 시케줄링한다. 우선순위는 1~10으로 1이 가장 낮은 우선순위고, 10이 가장 높다.

스레드 덤프 분석하기

Java 애플리케이션의 스레드 덤프는 아래의 명령어로 가능하다.

jstack [옵션] pid
옵션 설명
-I 락에 관한 추가적인 정보를 출력한다.
-F -I 옵션이 응답하지 않으면 강제 실행시킨다.
-m Java와 C/C++ 프레임의 스택 트레이스를 혼합하여 출력한다.
-h,-help 도움말 메시지를 출력한다.

스레드 덤프 읽어보기

아래의 코드를 실행 시켜 스레드 덤프를 읽어봤다.

public class Application {

    public static void main(String[] args) {
        ZeroThread zeroThread = new ZeroThread();
        
        zeroThread.start();
    }

    private static class ZeroThread extends Thread {
        @Override
        public void run() {
            System.out.println("Running Zero Thread");
            try {
                Thread.sleep(300000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

결과는 다음과 같다.

first-thread-dump

  1. 개행 단위로 각 스레드의 정보를 담고 있다.
    • "Thread-0": Thread 이름이 Thread-0이다.
    • #10: 식별자 10번을 가지고 있다.
    • prio=5: 우선 순위가 5다.
    • tid=0x00007f97f9861000: Thread가 점유하고 있는 메모리 주소를 의미하는 Thread ID다.
    • nid=0xa103: OS에서 관리하는 Thread ID다.
  2. Thread.sleep()이 완료되기 전에 스레드 덤프를 캡쳐했기 때문에, TIME_WAITING 상태다.
  3. 데몬 스레드의 경우 따로 표기된다.

경합 상태 진단하기

경합 상태 진단할 때는 RUNNABLE 상태가 긴 Thread와 BLOCKED 상태가 긴 Thread가 없는지 확인해야된다. 예시를 통해 확인해보자.

public class Application {

    public static final Object object1 = new Object();

    public static void main(String[] args) {
        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

        thread2.start();
        thread1.start();
    }

    private static class FirstThread extends Thread {

        @Override
        public void run() {
            synchronized (object1) {
                System.out.println("First Thread has object1's lock");
            }
        }
    }

    private static class SecondThread extends Thread {

        @Override
        public void run() {
            synchronized (object1) {
                System.out.println("Second Thread has object1's lock");
                int a = 2;

                while(true) {
                    a = (a + 2) % 10000;
                }
            }
        }
    }
}

위 코드는 Second Thread가 먼저 object1을 소유하면서 무한 루프를 돌게되면서, First Thread가 object1을 영원히 기다리게 되는 상태다.

second-thread-dump

위 사진과 같이 Thread-0가 BLOCKED 상태, Thread-1이 RUNNABLE 상태인 것을 확인할 수 있다.

데드락 진단하기

스레드 덤프로 쉽게 데드락을 찾아낼 수 있다. 아래의 코드를 실행해보자.

public class Application {

    public static final Object object1 = new Object();
    public static final Object object2 = new Object();

    public static void main(String[] args) {
        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

        thread1.start();
        thread2.start();
    }

    private static class FirstThread extends Thread {

        @Override
        public void run() {
            synchronized (object1) {
                System.out.println("First Thread has object1's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("First Thread want to have object2's lock. so wait");

                synchronized (object2) {
                    System.out.println("First Thread has object2's lock too");
                }
            }
        }
    }

    private static class SecondThread extends Thread {

        @Override
        public void run() {
            synchronized (object2) {
                System.out.println("Second Thread has object2's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second Thread want to have object1's lock, so wait");

                synchronized (object1) {
                    System.out.println("Second Thread has object1's lock too");
                }
            }
        }
    }
}

데드락이 발생하면 스레드 덤프에 아래 사진과 같이 발생했다고 알려준다.

third-thread-dump

참고 자료

https://d2.naver.com/helloworld/10963

https://devbox.tistory.com/entry/Java-%EB%8D%B0%EB%AA%AC%EC%93%B0%EB%A0%88%EB%93%9C

https://deftkang.tistory.com/56

https://math-coding.tistory.com/175

댓글남기기