본문 바로가기
Java/Java Basic

[Java Basic] 43 - Java Thread 2 - Thread 실행 제어 및 Daemon Thread

by Rosmary 2022. 9. 20.
728x90
반응형

 

 

 

지난 포스팅에서 Java Thread와 ThreadGroup의 우선순위를 지정하는 법 및 Thread 우선순위에 따른 Thread 실행 순서에 대해 확인해보았다. 하지만 우선순위로만은 Thread를 제어하기에 부족한 점이 많은데, 입력값을 받아야만 동작하는 Thread의 경우, 입력값이 들어올 때까지 자신의 Thread 실행을 일시 정지하거나 중단해야 할 필요가 있기 때문이다. 또한 사용될 것으로 예상되었던 Thread가 필요없어진 경우 Thread 중단 또는 삭제를 진행해야 하는 경우도 생긴다.

 

이번 포스팅에서는 생성한 Thread의 실행을 제어하는 방법에 대해 알아보고, Daemon Thread라고 불리는, 상위 ThreadGroup이 활성화되었을 때만 동작하는 Thread에 대해 간략하게 설명하려한다.

 

 

 

1. Thread 상태 제어 매서드

 

Thread는 실행 관련 상태값 4가지를 가진다. 

 

*  생성(NEW)                    : 생성 후 한 번도 실행되지 않은 Thread.   start() 매서드에 의해 RUNNABLE 상태로 진입

*  실행대기(RUNNABLE)  : start() 매서드에 의해 실행 대기 상태인 Thread.

                                            실행 후, 동시에 실행되는 다른 Thread가 존재하는 경우 일시 정지 상태에 들어감.

                                            (suspend(), sleep(), wait(), join() 등의 매서드로 일시정지 상태로 진입)

 

*  일시정지(WAITING, BLOCKED): 다른 Thread 실행을 위해 실행이 중단된 Thread.

                                             (time-out, resume(), notify(), interrupt() 등의 매서드로 다시 RUNNABLE 상태 진입)

*   종료(TERMINATED):   실행이 완료된 Thread의 상태.

 

 

 

 

위 코드는 실행 제어에 대한 내용이 존재하지 않는 Thread 실행 코드다. 따라서 Test1과 Test2의 시작, 종료 문구가 main 매서드 실행할 때마다 다르게 출력된다. thread1에 실행 제어 매서드를 적용하여 변화를 확인해보려한다. 단, start()의 경우 이전의 포스팅에서 계속해서 다루어 왔던 내용이라 건너뛰고, 나머지 매서드들에 대해 알아보려 한다.

 

 

 

(1) sleep() - Thread의 실행을 특정 시간동안 일시 정지

 

sleep()은 실행중인 Thread를 잠시 재우는, 말 그대로 일시 중단하는 매서드다. sleep() 매서드는 static으로 정의되어 있기 때문에 인스턴스 생성없이 Thread 클래스로부터 바로 호출하여 사용한다. 인스턴스를 통한 호출이 에러를 유발하지는 않지만 static 제어자가 적용되어 있어, 항상 매서드를 호출한 Thread의 실행을 중단한다는 특징이 있다. 

 

 

 

 

위의 코드를 보면 sleep 매서드가 10이라는 매개 변수와 함께 호출이 된 것을 확인할 수 있다. thread1 인스턴스를 통해 호출된 매서드이나, 매서드가 static 제어자가 적용되어 있으므로 thread1에 영향을 주는 것이 아니라 이 매서드를 호출한 main 매서드를 실행하는 Thread에 영향을 주게 된다. 따라서 [Sleep Test]라는 문구도 main 매서드가 실행된 지 10초가 지나서야 나머지 두 개의 Thread와 동시에 진행되는 것이다. 만약 thread1의 실행을 10초간 중단하고 싶다면 Test2 코드 내에 Thread.sleep을 적용해주어야 한다.

 

 

Thread 시작은 동시에 진행되었으나 sleep() 매서드로 인해 Test1이 10초 뒤에 Terminated 되었다.

 

 

 

(2) Interrupted - Thread 실행 취소

 

인터넷에서 본인 확인 시 사용하는 핸드폰 인증번호 입력란을 떠올려보자. 인증번호가 문자로 전송되면 3분 이내로 인증번호를 입력해야만 한다. 이 절차에 대해 아주 간략하게 코딩을 해보자.

 

 

 

인증번호를 입력받는 코드는 main Thread로 진행하도록 설정했고, 인증번호 입력 시간을 체크하는 코드는 별도의 Thread에서 진행하도록 설정했다. 그런데, 필자는 분명 인증번호를 입력하였음에도 불구하고 인증번호 입력 시간이 초과했다는 문구가 출력된다. 즉, 인증번호가 입력이 완료되면 CountDown Thread는 중단이 될 필요가 있다. 

 

지금과 같이 진행중인 Thread 내 코드가 실행이 완료되기 전에 취소를 시켜야 할 때 사용하는 매서드는 interrupt()다. 이 매서드는 sleep()과 달리 인스턴스 매서드로 정의되어 있기 때문에 특정 Thread 인스턴스를 통해 호출하여 그 Thread만 실행 취소를 하는 것이 가능하다. 하지만 interrupt() 매서드로 실행 취소를 하는 행위는, Thread 자체의 상태값만 변경하는 것이기 때문에 단독으로 interrupt()를 사용해봐야 아무런 변화가 나타나지 않는다.

 

여전히 입력 시간 초과 문구가 나타난다.

 

 

여기서 countdown의 interrupt 상태를 확인해보자. 이를 확인하는 매서드명은 isInterrupted()다. 

 

 

 

countdown의 interrupt 여부가 false에서 true로 변경된 것이 보인다. 그럼 isInterrupted() 매서드를 CountDown 코드 내에 적용해주면 이 문제는 쉽게 해결된다. 하지만 Runnable은 추상매서드인 run()만 가지기 때문에 isinterrupted()를 적용하려면 CountDown 클래스가 Thread 클래스를 상속받도록 코드를 수정해주어야 한다.

 

 

 

 

이제 코드를 실행해보자. 그러나 예상과 달리 카운트다운은 멈추지 않는다. 왜??

 

간혹 interrupt() 호출 뒤에도 isInterrupted 출력값이 false로 나오는 경우도 있다. 아래에 설명.

 

 

Thread의 interrupt 매서드를 사용하면, CountDown은 InterruptedException이 발생하게 된다. 그런데 CountDown 코드 내에 Thread.sleep()으로 인해 InterruptedException에 대한 예외처리가 되어있다. sleep() 매서드에 의해 Thread가 잠시 멈춰진 상태에서 interrupt() 매서드가 호출되면 for 문 안에 위치한 Thread.sleep()에 의해 interrupt가 호출된 것과 동일한 효과가 나타난다.  위에서 언급했듯이 interrupt는 Thread 자체의 상태값을 변경할 뿐, 강제로 종료할 수 있는 매서드가 아니기 때문에 run()에 의해 코드가 지속적으로 실행되면서 isInterrupted() 값이 false로 초기화된다.

 

따라서 InterruptedException 예외 코드 내에 다시 Interrupt() 문을 작성하여 이 문제를 해결하면 된다.

 

 

 

isInterrupted()와 유사한 매서드로 interrupted() 매서드가 있는데, 이 매서드는 interrupted된 매서드의 상태값을 반환한 뒤, false로 초기화한다. 따라서 interrupted() 매서드를 위의 코드에 적용하려면 for 문 대신 while로 작성해야하며, 조건문에 interrupted() 결과값이 연산에 포함되어야 한다.

 

예외처리 블럭의 interrupt()문에 의해 일시적으로 interrupted() 값이 true로 변경되므로 while 문을 빠져나오게 된다.

 

 

(3) stop(), suspend(), resume() - Deprecated

 

stop(), suspend(), resume()은 Thread 내에 정의된 매서드이나 보안상의 문제로 인해  jdk 1.2 버전부터 전부 Deprecated 되었다. 따라서 Thread 실행을 제어하기 위해서는 이들 매서드를 활용한 별도의 클래스와 코드를 작성해야하는데, 이는 조금 뒤에 알아보려고 한다. 우선 Deprecated된 매서드의 사용 방법부터 알아보자.

 

* stop()       :  Thread의 중지. Thread 상태를 TERMINATED 상태로 전환

* suspend() :  Thread의 일시 중지. Thread 상태를 WAITING, BLOCKED 상태로 전환.

* resume()   : suspend()로 일시 중지된 Thread의 실행 재개. Thread 상태를 RUNNABLE 상태로 전환

 

stop()과 앞서 보았던 interrupt() 매서드 사이 차이점에 대해 필자도 상당한 고찰을 진행했다. "둘 다 Thread를 중지하는데, 무슨 차이가 있는 것일까"에 대해서 말이다. 코드를 여러 번 작성해서 진행해보니,  stop()은 실제로 Thread 자체를 중단시키는데 반해, interrupt()는 단순히 InterruptedException을 일으킨다. 즉, interrupt()는 직접적으로 Thread를 중단시키지 않으나, 중단을 위한 예외를 발생시킨다. 앞서 본 코드에서 필자가 Interrupted Exception 예외 처리를 했던 부분을 상기하자. 이러한 이유 때문에 사실상 Deprecated된 stop()를 직접 사용하기보다는 interrupt() 매서드를 활용한 별도의 stop() 매서드를 만들어 사용하는 경우가 많다. 이 역시 뒤에서 확인해 볼 예정이다.

 

Thread를 하나 만들어보자. 하나는 Thread 클래스를 상속받은 클래스인데, 단순히 문자를 1초마다 출력하는 기능을 한다. main Thread는 1부터 10까지의 숫자를 출력하는 for 문을 코드로 작성했다. main Thread는 우선순위를 10으로, Thread 클래스는 우선순위를 1로 지정했다.

 

맨 처음 Thread 실행 시, A가 먼저 출력되는 경우도 있다.

 

 

현재는 Thread 클래스가 start() 호출 시작 시간부터 중단을 요청하는 코드가 없기 때문에 main Thread와 거의 동시에 Terminated 상태로 전환된다. 이제 필자는, main Thread 내 for 문에서 i 값이 일 때 test1 객체(Thread 인스턴스)의 실행을 suspend()로 일시 중단할 것이고, 5일 때 resume()으로 다시 실행을 재개할 것이다. 이렇게되면 Main 함수가 종료되더라도 test1은 10번의 'A' 출력을 끝내기 전까지 계속 화면에 나타날 것이다.

 

 

이번에는 필자가 i값 7일 때 stop() 매서드를 적용하여, test1 Thread를 아예 Terminated 상태로 돌려버리려한다. 이렇게 되면 test1 Thread는 마치 interrupt() 매서드를 사용한 것 마냥 중단된다.

 

 

 

이제 코드를 조금 변경하여, 출력되는 내용을 Thread의 상태로 변경해보려한다. 

 

 

 

suspend()와 resume(), stop()의 차이가 한 눈에 들어온다. 하나 유심히 봐야 할 부분이 있는데, test1 Thread가 정상적으로 실행되고 있는 상황에서 main Thread와 test1 Thread가 바라보는 getStack()의 값은 각각 TIMED_WAITING과 RUNNABLE로 서로 다르다. 이유는 정말 단순한데, main Thread 입장에서는 자기 자신의 코드가 실행중이기 때문에 test1이 실행은 되지만 일시적인 중단 상태(TIMED_WAITING) 상태에 놓여있다고 판단하는 것이고, test1 Thread 입장에서는 자신의 Thread에서 코드가 정상적으로 수행되고 있기 때문에 RUNNABLE이라는 결과를 돌려주는 것이다. 둘 다 동일한 상태를 의미하나 관점에 따라 다른 이름으로 불린다고 알고 있으면 된다. 

 

Thread 실행을 제어하는 이 매서드들이 왜 Deprecated 되었는지는 이곳에 영문으로 자세히 설명이 되어 있다. 세세한 해석은 필자가 번역가도 아니니 할 수 없지만 대략적인 내용을 보면 이렇다. 이들 매서드를 직접적으로 사용하면, Thread 실행 전환 과정에서 lock, deadlock 등이 걸리는 일이 빈번하며, 그로 인해 객체에 손상이 일어난 채로 실행될 수 있으므로 안전하지 않기 때문이라고 한다(영어를 하도 안봐서 그런건지, 아니면 오늘따라 피곤한건지 영 해석하기가 싫다).

 

[[ 참고 ]]

* lock : 하나의 Thread가 공유자원을 사용 중일 때 다른 Thread에서 해당 공유자원으로의 접근을 차단하는 것을 의미하는 용어. 이 때 lock이 걸린 Thread는 block 되었다고 말한다.

 

* deadlock: 여러 Thread가 공유자원의 접근을 기다리다가 전부 block 상태에서 벗어나지 못하는 상황을 일컫는 용어. 사거리에서 꼬리물기하는 차량을 비유로 많이 사용한다.

 

 

따라서 suspend(), resume(), stop()은 별도로 클래스를 만들고, 해당 클래스 내에 일시정지, 중단 등의 변수를 boolean으로 지정하여, suspend(), resume(), stop()이 이 boolean 값을 변경하도록 수정한다. 이 부분은 조금 뒤에 다시 살펴보자.

 

 

 

(4) yield() 매서드 - 남은 실행 시간을 포기하고 다른 매서드에게 자원 양보

 

싱글 코어 CPU에서 여러 Thread가 실행되는 경우, 각 Thread들은 짧은 시간동안 CPU를 점유하는 과정을 거친다. 

 

이미지 출처: http://www.qnx.com/developers/docs/qnxcar2/index.jsp?topic=%2Fcom.qnx.doc.neutrino.getting_started%2Ftopic%2Fs1_procs_Multiple_threads_single_CPU.html

 

 

위의 그림(그림판으로 그리기 귀찮아서 출처 이미지를 조금 손봤다)에서 C, X, W를 각각 Thread라고 보자. C의 경우 우선순위가 높아 CPU를 점유하는 시간이 길고 X, W는 거의 엇비슷하다. 만약, 우선순위가 높은 C 대신 X가 급하게 처리되어야한다면 어떻게 해야할까? C의 실행을 잠시 중단해야하는데, sleep()을 사용할 경우 할당받은 CPU 점유시간이 끝나야만 sleep() 매서드가 적용되고, interrupt() 매서드를 사용하면 Thread 자체가 아예 취소 상태로 돌리기(는 하지만 실제로 취소가 되려면 코드를 조금 더 짜야된다. 위에서 봤듯이)만 할 뿐이라 소용이 없다. 적합한 CPU 점유시간 내에 실행중인 Thread를 일시 중단하는 매서드가 바로 yield()라는 이름을 가진 매서드다. 

 

yield는 산출물, 수득이라는 의미도 가지지만 Java에서는 도로 표지판에서 흔하게 볼 수 있는 양보의 뜻을 지닌다. "즉, 내가 일 하고 있는 중이었지만 다른 Thread에게 잠시 자리를 양보할게!"라는 의미다. 

 

yield()는 sleep()과 마찬가지로 static 매서드다. sleep() 매서드 예제에서 확인했지만, Thread 클래스의 static 매서드는 코드가 실행되는 매서드에만 영향을 끼친다. yield() 매서드를 사용하면, 현재의 Thread를 다른 Thread에게 자리를 양보하고 자신의 코드는 실행을 잠시 중단한다.

 

 

실제 코드를 통해 확인해보자.

 

 

 

코드는 단순하다. main Thread는 B를 계속 출력하고, 필자가 추가로 생성한 Test Thread는 A만을 출력하도록 코드를 작성했다. 그리고 main Thread는 B가 100번째 출력될 때마다 yield() 매서드를 적용하여, Thread의 상태가 어떻게 변하는지 확인했다. 

 

위의 코드에서는 main Thread가 먼저 실행되었다. 하지만 main 내부 for 문의 증감값인 i가 0으로 if 조건 "i % 100 == 0"을 만족하기 때문에 main Thread는 자신에게 부여된 실행 시간이 남았음에도 불구하고 자신의 Thread를 중지하는 것이 확인된다. 첫 줄에서 B 출력 이후 A가 연달아 출력되는 결과를 확인하자.

 

콘솔 결과에서 두 번째 줄도 마찬가지다. 화면이 작아서(?) 짤리긴 했는데, 실제 두 번째 줄에 출력된 A, B 문장을 긁어다가 B의 갯수를 세어보면 정확히 100개다. 즉, B가 계속해서 실행되는 중에 yield() 호출로 Test1 Thread에게 자원을 양보했다는 이야기다. 

 

위의 예시를 보아서 알겠지만 사실 yield()는 단독으로 사용되기에 무리가 있는 매서드다. Thread 간의 실행, 대기가 워낙 빠르게 일어나다보니 특정 시점을 잡아 yield() 매서드를 적용하기가 당연히 쉽지 않다. yield()의 활용법은 Deprecated 매서드의 활용과 함께 본 포스팅의 후반부에서 다룰 예정이다.

 

 

 

 

(5). join() - 특정 Thread의 실행이 완료될 때 까지 대기

 

yield() 또는 interrupt() 매서드와 유사한듯 약간 다른 join()에 대한 설명이다. 우선 join()은 특정 매서드가 종료될 때까지 join() 매서드를 실행하는 Thread를 중단시키는 역할을 한다. 만약 main() 매서드 내에서 thread2.join()을 호출하면, main Thread는 thread2가 실행 종료되어 TERMINATED 상태로 들어가기 전까지 동작을 하지 않고 대기 상태에 놓이게 된다.

 

Thread2개에 대해 실행에 걸리는 시간을 측정해보는 코드를 작성해보자. main Thread에서 코드 실행 시간을 측정하려 한다. 코드는 아래와 같이 간단하게 작성했다.

 

 

 

이제 필자가 따로 박스를 치지 않더라도 어디에서 문제가 발생했는지 알 수 있을 것이다. main Thread에서 전체 코드의 실행 시간을 측정하는데, test1, test2 Thread와 별개로 마이웨이 행보를 보이다보니, endTime을 바로 측정해버리게 된다. 즉, main Thread는 test1과 test2가 완전히 종료될 때까지 기다렸다가 endTime을 측정해야하지만 위에서는 그 코드가 누락되다보니 문제가 발생한 것이다. 

 

이제 join() 매서드를 적용해보자. main Thread가 test1, test2 Thread의 종료를 기다려야하기 때문에 test1.join(); test2.join()을 추가해주면 된다. join()의 경우 sleep()과 동일하게 InterruptedException이 적용되어야하므로, 이에 대한 예외처리도 진행해주어야 한다.

 

 

 

yield(), sleep(), join(), interrupt()의 차이는 글로만 보면 절대로 와닿지 않으니 반드시 코드로 작성해서 확인해보도록 하자. 

 

 

 

2. Deprecated Thread 매서드의 활용

 

stop(), suspend()와 같이 Deprecated된 매서드의 문제 중 하나는 Thread의 CPU 점유 시간이 완전히 끝나야만 종료 및 일시 중지를 할 수 있다는 것이다(이로 인해서 lock과 deadlock이 빈번하다는데...조금 더 확인해봐야한다). stop()의 경우 interrupted()로, suspend()는 yield(), join()으로 대체할 수 있으나, 위에서 살펴보았듯이 interrupted()는 상태 변화만 이끌어내며, yield(), join()은 정작 다른 Thread가 CPU 점유를 종료하거나 실행이 완료되어야만 재개되는 특성상 재개 시점을 명확하게 정의할 수 없다는 문제가 있다. 

 

이 때문에 위에서 보았던 모든 매서드를 Thread 클래스를 상속받는 클래스에 이들 매서드를 재정의하여 사용한다. 그리고 stop(), suspend(), resume()은 잠깐 전에 보았던 문제점으로 인해 직접적으로 매서드를 사용하지 않는다. 어떻게 쓸까? 우선 코드부터 보자. 필자는 Thread를 시행할 코드를 하나 만들것인데, run()외에도 stop(), suspend(), resume() 매서드를 재정의하려한다. 오버라이딩은 불가능한데, Thread의 실행 제어 매서드들은 final 제어자가 적용되어 있기 때문이다.

 

 

 

 

의외로 코드는 단순하다. stop, suspend에 즉각적인 대응을 하기 위해 이들을 인스턴스 변수로 만들었다. run() 매서드에는 while 문이 존재하는데, while 문에 들어간 조건들이 이들 변수기 때문에 매서드 실행 시 이 값의 변경에 따라 즉각 중단과 취소가 가능해진다. run() 매서드 코드에서 실제로 화면에 출력되는 내용은 System.out.printf() 문이 1초마다 실행되는 것 뿐이다.

 

이 클래스를 인스턴스화하여 실제로 중지, 일시 정지 및 재개가 가능한지 확인해보자.

 

 

 

Thread 클래스 부분에서 눈여겨 볼 부분은 suspend() 대응 매서드인 mySuspend()로 인해 실행되는 else 블록 내부 코드다. suspendCheck가 true 값으로 지정되는 경우 if-else의 else 문이 실행되는데, yield() 매서드를 통해 현재 Thread 클래스의 CPU 점유 자체를 할 수 없도록 정의되어 있다. 

 

 

 

3. Daemon Thread

 

이번 포스팅의 마지막 내용인 Daemon Thread에 대해 알아보자. Daemon이라는 단어는 의미상으로는 "악마"를 의미하나, 무슨이유에서인지 IT 분야에서는 "서비스"를 일컫는다. 리눅스를 사용해보신분들이라면 많이 들어보았을 단어일 것이다.

 

Java에서의 Daemon Thread는 서비스를 제공하는 Thread로 해석한다. 직역하면 이렇고, 의역하자면 다른 Thread에 종속되어 보조적으로 동작하는 Thread다. 그렇기 때문에 Daemon Thread의 경우 종속된 Thread가 끝나면 자신의 임무가 완료되지 않았더라도 함께 TERMINATED 상태로 전환된다.

 

지난 포스팅의 서두에서 소개한 내용 중,  워드나 PPT의 자동 저장이나, 메일함의 새 메일을 팝업하는 등의 기능이 이 Daemon Thread와 매우 유사하다. 워드/PPT의 경우, 워드를 종료하면 작성하던 문서를 자동으로 저장하는 기능 역시 종료되며, 메일 서버 역시, 주 서비스인 메일 서비스가 종료되면 새 메일을 탐색하고 팝업하는 부가적인 기능 역시 중단된다.

 

그럼, Daemon Thread는 어떻게 만들까? 매우 간단하다. Thread 클래스 내에 정의된 setDaemon() 이라는 메서드를 사용하면된다. 매개변수로 boolean 값을 입력받는데, Daemon Thread로 지정할래? 말래?를 묻는 것이기 때문에 true를 사용하면 Daemon Thread로 지정할 수 있다. 간단한 예시를 보자.

 

 

 

main과 test1  Thread 모두 1초마다 특정 문자열을 출력하도록 하는 코드다. 단, main 은 10초 간, test1 Thread는 15초 간 동작하도록 만들었다. Daemon이 적용되지 않은 Thread는 main Thread가 끝났음에도 불구하고 test1 Thread는 계속 돌고 있다. 이제 test1 Thread를 Daemon으로 만들어보자.

 

 

 

main Thread가 종료되고도 계속 동작했던 test1 Thread가, Daemon으로 지정되자마자 main Thread를 따라 바로 종료가 되는 것을 확인할 수 있다. 참고로 특정 Thread가 Daemon인지 아닌지는 isDaemon() 매서드로 확인하면 된다.

 

 

 

setDaemon()으로 Daemon Thread가 되면 반드시 main Thread에만 종속되는 것이 아니다. 아래와 같이 Thead 클래스를 변경해보자.

 

 

 

굳이 필자가 설명하지 않더라도 의도가 이해될 것이라 생각한다. 이제 필자는 이 클래스부터 인스턴스를 2개 생성할 것인데, 하나는 num, name만 매개인자로 생성한 인스턴스고, 다른 하나는 RemakeThread 타입까지 매개인자로 생성한 인스턴스다.

 

 

 

클래스 코드를 보면 test2 Thread 안에 test1 Thread가 포함된 상태다. 만약 test1이 Daemon Thread로 지정되지 않는다면, test2는 10까지, test1은 15까지 숫자가 출력될 것이다. 하지만 현재 test2 객체 내에 포함된 test1은 Daemon Thread로 지정되어 있기 때문에 test2에 종속이 된 상태다. main Thread가 일치감치 종료되었음에도 test2에 종속된 test1이 Thread를 종료하지 않았음을 확인하자.

 

 

필자가 되도록이면 입력하는 문자열을 주기적으로 txt 파일로 자동 저장하는 코드도 예시로 올리려 했는데... 이건 각자 해보자.

 

 


 

 

다음 포스팅에서는 Thread의 동기화에 대해 알아보려 한다.

 

 

Fin.

반응형

댓글