[JAVA]데드락 Thread Dump 떠보기

무지성개발자·2023년 8월 17일

Dead Lock이란

데드락은 교착상태를 말한다. 교착상태는 서로 이도저도 못하고 가만히 있는 상태를 말한다.

데드락은 멀티스레드 환경에서 일어나는데 최소 2개 이상의 쓰레드가 공유하는 작업에서 서로의 작업이 끝나길 기다릴 때 발생한다. 코드로 보면 이해하기 편하니 Thread Dump까지 진행할 코드를 먼저 봐보자.

public class Deadlock {
    private static final Object LOCK_1 = new Object();
    private static final Object LOCK_2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (LOCK_1) {
                System.out.println("1번 쓰레드 : Acquired LOCK 1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                }
                System.out.println("1번 쓰레드 : Waiting for LOCK 2");
                synchronized (LOCK_2) {
                    System.out.println("1번 쓰레드 : Acquired LOCK 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (LOCK_2) {
                System.out.println("2번 쓰레드 : Acquired LOCK 2");
                System.out.println("2번 쓰레드 : Waiting for LOCK 1");
                synchronized (LOCK_1) {
                    System.out.println("2번 쓰레드 : Acquired LOCK 1");
                }
            }
        });

        Thread thread3 = new Thread(() -> {
            try {
                Thread.sleep(200);
                System.out.println("3번 쓰레드도 출발");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (LOCK_1){
                System.out.println("3번 쓰레드 : Acquired LOCK 1");
            }
        });

        thread1.setName("1번 쓰레드");
        thread2.setName("2번 쓰레드");
        thread3.setName("3번 쓰레드");
        thread1.start();
        thread2.start();
        thread3.start();

        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
        }

        System.out.println("Main thread finished");
    }
}

코드를 간단하게 요약하면 1번 쓰레드가 LOCK_1로 잠긴 작업 중에 LOCK_2를 획득 하려 하는데, 2번 쓰레드 역시 LOCK_2로 잠긴 작업 중에 LOCK_1를 획득하려고 한다. 서로가 서로의 LOCK객체를 획득하려는데 서로의 작업이 끝난 상태가 아니니 둘 다 LOCK객체를 놓아주지 않아 무한정 대기 하게 된다. 교착상태에 빠졌으니 데드락이 발생한 것 이다.

데드락의 조건

  • 상호 배제(Mutual Exclusion) : 자원은 동시에 하나의 프로세스나 쓰레드만 사용해야 한다.

  • 점유와 대기(Hold and Wait) : 적어도 하나의 자원을 점유한 상태에서 다른 자원을 기다리는 상황이어야 한다.

  • 비선점(No Preemption) : 이미 점유된 자원을 다른 프로세스나 쓰레드가 강제로 빼앗을 수 없어야 한다.

  • 순환 대기(Circular Wait): 서로 다른 프로세스나 쓰레드들이 자원을 원형으로 대기하며 서로가 점유한 자원을 기다려야 한다.

데드락은 위 4개의 조건을 모두 만족해야한다.

코드에 좀 늦게 작업을 시작하는 3번 쓰레드1번 쓰레드가 사용 중인 LOCK_1를 획득하려고 하는데 3번 쓰레드도 데드락에 빠질까??

아니다. 3번 쓰레드는 데드락에 빠진게 아니라 그냥 대기 중일 뿐이다. 3번 쓰레드점유와 대기, 순환 대기의 조건을 충족하지 못하기 때문이다.

Thread Dump

Thread Dump는 VisualVM으로 진행한다.
코드를 실행하고 VisualVM에서 Thread탭을 보면 데드락을 감지했다고 빨갛게 알려준다.
그럼 어떤 쓰레드가 데드락에 걸린건지 3번 쓰레드는 정말 데드락에 빠진게 아닌지 확인해야하니 오른쪽 Thread Dump를 눌러보자.
솔직히 이해하기 힘든 말들이 엄청 써져있지만... 우린 제일 밑으로 가보면 위 이미지와 비슷한 내용을 볼 수 있다.

1번 쓰레드2번 쓰레드가 사용중인 Object를 모니터링 중이고 2번 쓰레드1번 쓰레드가 사용중인 Object를 모니터링 중이란 걸 알 수 있다. 그리고 3번 쓰레드는 정말 데드락이랑 관련이 없다는 것도 확인했다.

데드락을 풀어보자.

데드락은 4개의 조건을 모두 만족해야 발생하니 하나라도 조건을 만족 못하면 데드락에 빠지지 않는다.

상호 배제 조건을 배제 하는 건 synchronized 키워드를 뺴 여러 쓰레드들이 동시에 접근 하게 해서 데드락을 풀 수 있지만, 동시성 문제가 발생하니 PASS.

비선점 조건을 배제 하는 건 상대방이 작업 중에 억지로 락을 해제하여 자원을 뺏어 작업을 시작하면 다른 쓰레드랑 데드락이 발생할 수도 있으니 PASS.

public class Solution2 {
    private static final Object LOCK_1 = new Object();
    private static final Object LOCK_2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (LOCK_1) {
                System.out.println("1번 쓰레드 : Acquired LOCK 1");
                System.out.println("1번 쓰레드 : Waiting for LOCK 2");
                synchronized (LOCK_2) {
                    System.out.println("1번 쓰레드 : Acquired LOCK 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (LOCK_1) {
                System.out.println("2번 쓰레드 : Acquired LOCK 2");
                System.out.println("2번 쓰레드 : Waiting for LOCK 1");
                synchronized (LOCK_2) {
                    System.out.println("2번 쓰레드 : Acquired LOCK 1");
                }
            }
        });

        thread1.setName("1번 쓰레드");
        thread2.setName("2번 쓰레드");
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
        }

        System.out.println("Main thread finished");
    }
}

1번 쓰레드, 2번 쓰레드 둘다 LOCK_1으로 작업을 시작하고 LOCK_2자원을 사용하도록 변경했다. 이렇게 되면 둘 다 순차적으로 LOCK_1, LOCK_2의 자원을 사용하여 순환 대기조건을 배제 하여 해결했다.

하지만 이 방법은 모든 쓰레드들이 똑같은 순서의 자원은 사용하기에 다른 쓰레드들이 일을 못하고 기다려야해서 성능이 떨어질 수 있는 단점이 있다.

public class Solution1 {
    private static final Object LOCK_1 = new Object();
    private static final Object LOCK_2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (LOCK_1) {
                System.out.println("1번 쓰레드 : Acquired LOCK 1");
                System.out.println("1번 쓰레드 : Waiting for LOCK 2");
            }
            synchronized (LOCK_2) {
                System.out.println("1번 쓰레드 : Acquired LOCK 2");
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (LOCK_2) {
                System.out.println("2번 쓰레드 : Acquired LOCK 2");
                System.out.println("2번 쓰레드 : Waiting for LOCK 1");
            }
            synchronized (LOCK_1) {
                System.out.println("2번 쓰레드 : Acquired LOCK 1");
            }
        });

        thread1.setName("1번 쓰레드");
        thread2.setName("2번 쓰레드");
        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
        }

        System.out.println("Main thread finished");
    }
}

자원을 점유 중에 또 다른 자원을 얻으려는 코드를 밖으로 빼서 점유와 대기의 조건을 배제해서 해결했다.

전체 코드 - 깃 허브


한 줄평 : 자바 쓰레드만으로 데드락을 알아봤는데 DB를 연결해서 Transaction까지 붙는다면 어디서 데드락이 발생했는지 찾는건 끔찍할 것 같다.

참고 - https://www.crocus.co.kr/524

profile
no-intelli 개발자 입니다. 그래도 intellij는 씁니다.

0개의 댓글