통신 상태를 확인하기 위한 명령어, 서버 상태를 확인하기 위한 명령어에 이어서 마지막으로 애플리케이션 진단을 위한 명령어를 알아본다. Java 웹 애플리케이션이 비정상적으로 느리게 동작하는 경우, 스레드 덤프를 분석해 봐야 된다.
스레드 덤프를 분석하는 이유
웹 애플리케이션에는 동시에 수 많은 사용자의 요청을 처리하기 위해서 여러 개의 스레드를 사용한다. 이렇게 동시에 여러 개의 스레드가 동작하면 경합 상태가 발생할 것이며, 데드락도 발생할 가능성이 높다.
- 경합 상태: 여러 개의 스레드가 공유 자원을 동시에 사용하는 상황
- 데드락: 경합 상태일 때 스레드들이 서로 작업이 끝나기를 기다리고 있어, 작업이 멈추어 버리는 상황
Java 스레드
스레드 동기화
Java에서는 경합 상태일 때 데이터의 정합성을 보장하기 위해 스레드 동기화로 한 번에 하나의 스레드만 공유 자원에 접근할 수도록 한다. 이를 구현하기위해 Java에서는 각 인스턴스들이 자신의 Monitor를 가지게하고, 스레드가 이 인스턴스를 사용할 때 Monitor를 점유하도록 한다. 이 Monitor는 하나의 스레드만 점유할 수 있기때문에, 다른 스레드들은 Wait Queue에서 기다리게 된다.
스레드 상태
Java에서는 스레드의 상태를 Enum으로 선언되어있다. 링크
- 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();
}
}
}
}
결과는 다음과 같다.
- 개행 단위로 각 스레드의 정보를 담고 있다.
"Thread-0"
: Thread 이름이 Thread-0이다.#10
: 식별자 10번을 가지고 있다.prio=5
: 우선 순위가 5다.tid=0x00007f97f9861000
: Thread가 점유하고 있는 메모리 주소를 의미하는 Thread ID다.nid=0xa103
: OS에서 관리하는 Thread ID다.
Thread.sleep()
이 완료되기 전에 스레드 덤프를 캡쳐했기 때문에, TIME_WAITING 상태다.- 데몬 스레드의 경우 따로 표기된다.
경합 상태 진단하기
경합 상태 진단할 때는 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을 영원히 기다리게 되는 상태다.
위 사진과 같이 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");
}
}
}
}
}
데드락이 발생하면 스레드 덤프에 아래 사진과 같이 발생했다고 알려준다.
참고 자료
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
댓글남기기