김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 : Interrupt, Yield

jkky98·2024년 8월 8일
0

Java

목록 보기
37/51

Thread 중단

public class ThreadStopMainV1 {

    public static void main(String[] args) {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "work");
        thread.start();

        sleep(4000);
        log("작업 중단 지시 runFlag=false");
        task.runFlag = false;
    }

    static class MyTask implements Runnable {

        volatile boolean runFlag = true;

        @Override
        public void run() {
            while (runFlag) {
                log("작업 중");
                sleep(3000);
            }
            log("자원 정리");
            log("자원 종료");
        }
    }
}

위의 코드에서,

MyTask 객체는 "work"이름을 가진 thread에서 runFlag가 true일 경우 계속해서 로직을 반복한다.

main 스레드는 "work" 스레드를 만들고 실행시킨다.

그리고 4초 후 (아마 work스레드가 2번 째 반복중인 상태(sleep 상태)) runFlag를 false로 만든다.

work 스레드는 3번째 루프에서 while문을 종료하고 두 개의 로그(자원 정리, 종료)를 출력하고 종료한다.

두 개의 스레드(main, work)로 하여금 제어변수(runFlag)를 만들어 main스레드가 work스레드를 통제하는 모양새의 코드이다.

Thread.sleep()을 통해 만든 간단한 멀티스레드 시나리오 코드이다. runFlag의 변화를 주어 다음 루프에서 멈추게하는 시나리오인데, 즉각적으로 스레드의 행동을 멈추게할 순 없을까?

thread.interrupt()

public static void main(String[] args) {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "work");
        thread.start();

        sleep(100);
        log("작업 중단 지시 thread.interrupt()");
        thread.interrupt();
        log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
    }

thread.interrupt()를 통해 work 스레드에 InterruptedException를 예외를 발생시킬 수 있다.

InterruptedException는 체크예외에 속하므로 개발자가 이를 던지거나 잡도록 해야한다.

쓰레드에 인터럽트를 발생시키면 쓰레드에 인터럽트 예외가 발생하여 즉각 예외를 처리(던져지거나, 잡는)하는 시나리오에 들어간다.

		@Override
        public void run() {
            try {
                while (true) {
                    log("작업 중");
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted() );
                log("interrupt messag= " + e.getMessage());
                log("state=" + Thread.currentThread().getState());
            }
            log("자원 정리");
            log("자원 종료");
        }

위의 코드에서, 외부에서 이 runuable한 작업을 수행하는 쓰레드에 인터럽트를 호출했다고 가정해보자.

위의 로직에 의하면 try문에서 인터럽트 예외 발생시 catch에 의해 인터럽트 예외는 잡힌다.

thread.interrupt()isInterrupted()로 확인할 수 있는 인터럽트 상태값은 true가 된다.

인터럽트의 동작 규칙은 다음과 같다.

  • 인터럽트 호출시 인터럽트 상태값을 true로 설정한다.
  • 인터럽트 상태값 true이면서 쓰레드가 join, sleep, wait와 같은 휴식 상태일 때 인터럽트 예외가 발생한다.
  • 인터럽트 상태값이 true여도 쓰레드가 휴식상태가 아닌 경우 로직은 계속된다.
  • 인터럽트 예외가 던져질 때 인터럽트 상태값은 true -> false로 다시 되돌아온다.

위의 코드에서 while문 내의 sleep()로직이 없다면 인터럽트 예외가 터지지 않는다.

위의 코드에서 인터럽트 예외를 잡고나서 인터럽트 상태값을 확인하고 있다. 그렇다면 당연히 인터럽트 상태값은 false일 것이나 실제로는

(1)인터럽트 발생, 상태값 변경 - (3)인터럽트 예외 발생, 상태값 원복의 과정을 거친 것이다.

isInterrupted()의 필요성

왜 인터럽트 예외가 catch되자마자 isInterrupted()는 false로 바뀌는 걸까?

InterruptedException이 발생한 뒤에도 인터럽트 플래그가 true로 남아 있으면 이후 sleep, wait, join 같은 블로킹 메서드를 호출할 때 즉시 또 InterruptedException이 발생해 개발자가 의도한 흐름이 깨질 수 있으므로, catch 블록에서 예외를 처리한 직후에 인터럽트 플래그를 명시적으로 false로 초기화(clear)하여 불필요한 예외 재발생을 방지해야 한다. 인터럽트 플래그를 명시적으로 초기화하는 것은 인터럽트 예외가 던져질 때 JVM이 알아서 초기화 해준다.

Thread.interrupted()

thread.interrupt()Thread.interrupted()는 잘 구분해야한다. 전자는 인터럽트 상태를 true로 만드는 것이고 Thread.interrupted()는 인터럽트 상태를 확인하고 곧바로 인터럽트 상태를 리셋(false)한다.

만약 인터럽트 상태가 true라면 true를 반환하고 인터럽트 상태를 곧바로 false로 만든다.

전자는 인터럽트 플래그 true로 변환, 후자는 인터럽트 플래그 확인후 false로 세팅하는 것이다.

@Override
        public void run() {
            while(!Thread.interrupted()) { // 인터럽트 상태 변경O
                log("작업 중");
            }
            log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted() );
            try {
                log("자원 정리");
                Thread.sleep(1000);
                log("자원 종료");
            } catch (InterruptedException e) {
                log("자원 정리 중 인터럽트 걸림");
            }
            log("작업 종료");
        }

위의 코드에서,

main에서 work 스레드에 인터럽트를 걸었다면 work 스레드의 인터럽트 플래그가 변화할 것이고 이를 즉각 while 반복문에서 체크할 수 있을 것이다. 플래그가 true가 되자마자 while문이 종료될 것이며 while문이 종료되자마자 인터럽트 상태는 false가 될 것이다.

Thread.yield()

어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다.

그런데 특정 스레드가 크게 바쁘지 않은 상황 이어서 다른 스레드에 CPU 실행 기회를 양보하고 싶을 수 있다. 이렇게 양보하면 OS 스케줄링 큐에 대기 중인 다른 스레드 가 CPU 실행 기회를 더 빨리 얻을 수 있다. 이를 yield()로 하여금 유도할 수 있다.

구체적으로 어떨 때 이 yield를 사용할 수 있을 지 아래의 코드를 참고해보면,

		@Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    if (jobQueue.isEmpty()) {
                        continue;
                    }

                    String job = jobQueue.poll();
                    log("출력 시작: " + job + ", 대기 문서: " + jobQueue);
                    Thread.sleep(3000);
                    log("출력 완료");
                }
            } catch (InterruptedException e) {
                log("q 입력 : 프린터 Thread 강제종료");
            }
        }

이 코드의 목적은 jobQueue에 데이터가 존재하면 이를 처리하고 싶고 데이터가 존재하지 않으면 처리하지 않는 것이다.

출력 로직에 CPU를 할당해서 빠르게 처리하는 것은 중요하지만 jobQueue에 데이터가 존재하는 지 확인하는 로직을 굉장히 빠르게 유지할 필요가 없을 것이다.

휴지통을 비우는 행동을 빠르게 비워버리는 것은 훌륭한 일이지만 이를 위해 1초마다 휴지통을 확인한다면 이는 굉장히 비효율적인 것과 같다.

이때 yield를 부여하여 OS에 "이 부분은 너무 신경쓰지 않아도 좋아"라는 힌트를 넘겨주어 OS가 해당 스레드의 자원할당을 적게 유지하도록 유도할 수 있다.(큐가 비어있는 동안)

즉 다음과 같이 수정한다.

@Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    if (jobQueue.isEmpty()) {
                    	// 추가 
                    	Thread.yield();
                        // 
                        continue;
                    }

                    String job = jobQueue.poll();
                    log("출력 시작: " + job + ", 대기 문서: " + jobQueue);
                    Thread.sleep(3000);
                    log("출력 완료");
                }
            } catch (InterruptedException e) {
                log("q 입력 : 프린터 Thread 강제종료");
            }
        }

이렇게 되면 "work"스레드는 큐가 비었을 때는 자원할당을 적게 받아 다른 쓰레드에 자원이 집중되도록 최적화할 수 있다.

우리는 이전에 자바는 Runnable 상태만을 알 수 있다고 했다.

즉 실행 준비 상태와 진짜 CPU가 작업하는 상태를 구분할 수 없다는 것이다. yield를 먹인다고 해서 스레드가 sleep()처럼 Runnable에서 Waiting으로 돌아가지는 않는다.

Runnable상태의 스레드에 CPU자원을 양보하면서 OS 스케줄링이 알아서 이 스레드를 상대적으로 외면하도록 유도하는 것이다.

정리

  • yield는 스레드가 Waiting으로 가지 않고 상대적으로 덜 로직이 진행되도록 유도할 수 있다.
  • 인터럽트와 인터럽트 상태, 인터럽트 예외를 이해하고 왜 자바는 인터럽트 예외를 강제로 잡게하는 지까지 이해할 수 있다.
  • 인터럽트 예외가 잡힌 후에 상태를 곧바로 리셋해야하는 이유를 이해할 수 있다.(그리고 이는 알아서 처리된다.)
profile
자바집사의 거북이 수련법

0개의 댓글