개발 블로그
article thumbnail
Published 2023. 4. 12. 00:16
[TIL] Day 9 항해99

오늘 겪은 문제

항해 측에서 제공해 준 강의로 자바 스레드를 학습했는데 강의에서 콘솔에 찍히는 결과와 내 인텔리제이 콘솔에 찍히는 결과가 달랐다.

 

<실행 코드>

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("task : " + Thread.currentThread().getName());
        };
        
        Thread thread = new Thread(task, "Thread");
        thread.start();
        
        thread.interrupt();
        
        System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
    }
}

왼쪽 : 강사님 콘솔  / 오른쪽 : 내 콘솔

원인을 추적하기 위해 몇 가지 가정을 하면서 다양한 시도를 해봤다.

 

시도한 것

가정 1) 나는 main thread가 out 필드를 먼저 점유하는데 반해 강사님은 task thread 가 먼저 점유

시도) main thread 가 out 필드 점유하기 전에 일시 정지로 만들어 task thread 가 점유할 수 있도록 하기

 

System 클래스의 out 필드가 static 필드이기에 여러 스레드가 공유하는 자원이라고 생각했고 멀티스레드 환경에서 이 자원을 어떤 스레드가 먼저 점유하느냐에 따라 출력 결과의 순서가 달라질 수 있겠다고 생각했다.

 

그래서 main thread 가 out 필드를 점유하기 전 일시 정지 상태로 만들어 task thread 가 out 필드를 먼저 점유할 수 있도록 만들었다.

thread.interrupt();

try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

System.out.println("thread.isInterrupted() = " + thread.isInterrupted());

그랬더니 의도한 대로 출력 결과의 순서는 강사님의 출력 결과 순서와 일치시킬 수 있었으나 thread.isInterrupted() 의 결과가 false 가 나왔다.(이 부분은 알게된 점에서 다루겠다)

 

 

가정 2) e.printStackTrace()는 그 후에 System.out.println()이 있으면 원래 나중에 실행된다.

시도) 싱글스레드 환경에서  Exception 을 발생시키고 e.printStackTrace() 를 catch 블록에서 실행한 후 바깥에서 System.out.println() 실행

public class Test {
    public static void main(String[] args) {
        try {
            String apple = "사과";
            Integer appleToInteger = Integer.pareseInt(apple);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Exception 이 발생했습니다.");
    }
}

가정했던 것과는 달리 정상적으로 e.printStackTrace() 의 결과가 먼저 콘솔에 나왔다.

 

 

가정 3) main thread 가 out 필드를 점유한 상황에서 task thread 역시 출력을 하려하니 어떤 충돌이 발생했다.

시도) main thread 에서 System.out.println() 을 주석처리하여 task thread 만 자원 사용하게 하기

 

thread.interrupt();
// System.out.println("thread.isInterrupted() = " + thread.isInterrupted());

그랬더니 정상적으로 e.printStackTrace() 결과가 먼저 콘솔에 출력되었다. 이 결과로 우선 출력에 있어 어떠한 충돌이 있다고 결론을 내렸고 printStackTrace() 메서드에서 그 충돌이 발생하지 않을까라는 생각이 들었다.

 

 

가정 4) printStackTrace() 메서드의 내부 동작에서 main thread 의 System.out.println() 과 충돌을 일으킨다.

시도) catch 블록에서 printStackTrace() 메서드 대신 getMessage() 메서드를 출력하도록 함

 

try {
    Thread.sleep(1000);
    System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
    System.out.println(e.getMessage());
}
System.out.println("task : " + Thread.currentThread().getName());

그랬더니 catch 블록 내부에 있는 출력이 먼저 진행되도록 결과가 잘 출력됐다.

 

여러 시도를 통해서 결국 main thread 의 System.out.println() 과 task thread 의 e.printStackTrace()가 동시에 이뤄지려는 과정에서 문제가 나타난다고 결론을 내릴 수 있었다.

 

마지막 확인을 위해서 e.printStackTrace()에 BreakPoint를 걸어 해당 코드 실행 전 main thread 가 System.out.println() 와 관련해 모든 자원을 반납했다는 것을 보장하여 디버깅을 해보았다.

역시나 catch {} 내부의 e.printStackTrace() 가 먼저 잘 나온 것을 볼 수 있다.

 

 

해결 방법

매니저님께 질문을 하면서 결국 이 문제에 대한 해답을 어느정도 찾았다.

 

콘솔에 출력될 String 들은 JVM 에 의해서 일단 메모리 버퍼에 저장된 후 화면에 출력된다.

그런데 out 과  err 는 서로 다른 버퍼를 사용하고 버퍼를 다루는 환경에 따라서 출력 결과가 달라질 수 있는 것이다.

 

마찬가지로 e.printStackTrace() 도 System.out 과는 다른 버퍼를 사용한다.

인텔리제이에서는 두 개의 버퍼를 서로 다른 두 개의 스레드에서 관리하는데 아마 두 스레드 사이에 우선순위가 존재함으로서 이런 결과가 나오게 되는 것 같다.

 

 

알게된 점

원인에 대해서 제대로 알아내지는 못 했지만 이것저것 시도하면서 알게 된 사실들이 있다.

 

1. interrupt flag 초기화 차이

가정 1번에 대한 시도 결과에서 thread.isInterrupted() = false 가 나왔던 이유를 조사해보니 이 역시 JDK 버전 차이에 의한 것이었다.

 

JDK 8 에서는 InterruptedException 이 발생하면 interrupt flag 를 false 로 초기화 하는데 반해 JDK 17 에서는 false 로 초기화 하지 않는다.

 

그로 인해서 thread.isInterrupted() 의 결과를 출력하기 전에 InterruptedException 이 발생한 경우 JDK 8 에서는 false가 JDK 17 에서는 true 가 나온 것이다.

 

 

2. 디버깅 옵션 Suspend : All vs Thread

여러가지 시도 중 디버깅을 통해서 확인해보려는 시도도 있었는데 이 때 Suspend 옵션을 All 로 선택하느냐 Thread 로 선택하느냐에도 차이가 있다는 것을 알게 됐다.

 

All 로 선택하여 디버깅을 하면 해당 breakpoint 에서 멈췄을 때 breakpoint 를 걸어둔 스레드를 제외한 모든 스레드가 일시 중지 상태로 들어간다.(All  : 모든 스레드를 중단할 것인가?)

 

반면 Thread 옵션을 선택해서 디버깅을 하면 해당 breakpoint 에서 멈추더라도 다른 스레드들은 모두 정상적으로 실행이 된다.(Thread : breakpoint 가 걸린 스레드만 중단할 것인가?)

 

아래 사진은 글 초기의 실행 코드에서 thread.interrupt() 코드 라인에 breakpoint 를 걸고 suspend 옵션을 다르게 선택했을 때의 출력 결과다.

suspend : All
suspend : Thread

 

 

 

 

'항해99' 카테고리의 다른 글

[TIL] DAY12  (0) 2023.04.15
[TIL] DAY 11  (0) 2023.04.13
[TIL] DAY 10  (0) 2023.04.12
[TIL] Day8  (0) 2023.04.10
[WIL] 항해 1주차  (0) 2023.04.09
profile

개발 블로그

@하얀.손

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!