Java Thread와InterruptedException

자바에서의 스레드는 아래와 같이 곱게 Sleep 되지 않는다. 항상 이때마다 try - catch를 통해 InterruptedException 를 처리해 주곤 했는데 이번 기회에 이에 대한 정리를 해본다.

Java Thread의 Interrupt

java에서의 interrupt의 개념은 다음과 같다. 한 스레드가 blocking된 다른 스레드를 깨우려는 신호를 보낼 때, Interrupt가 사용된다. 또한 각 스레드들은 자신이 현재 인터럽트 신호를 받았는지를 확인할 수 있다. 만약 아래와 같이 인터럽트 신호를 적절히 처리하지 않을 경우 원하는 동작이 수행되지 않는다.

public class WrongCase {
  static class WrongThread extends Thread {
    @Override
    public void run() {
      while (true) {
        System.out.println("do something...");
        System.out.println("Thread.currentThread().isInterrupted() = " + Thread.currentThread().isInterrupted());
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    Thread target = new WrongThread();
    target.start();
    Thread.sleep(2000);
    target.interrupt();
  }
}

보이는 바와 같이 main 스레드가 target 스레드에 target.interrupt를 통해 인터럽트를 날렸음에도 target thread는 아랑곳하지 않고 자신이 수행하는 Task를 계속해서 수행한다. 하지만 로그를 보면, Thread.currentThread().isInterrupted()의 값이 2초 후에 false에서 true로 변경 되는것을 확인할 수 있다. 즉, 인터럽트는 단순히 해당 스레드의 flag를 변경 시켜주고, 해당 flag에 대한 처리는 Thread의 run 메서드를 구현할 때 추가해줘야한다.

만약 어떤 스레드를 인터럽트 하고 싶으면 위 예제처럼 Thread.interrupt() 메서드를 호출하면 된다. 위 메서드가 호출 되면 대상 스레드는 아래의 동작 중 하나가 발생한다. 이는 공식문서를 근거로 작성된 내용이다.

  • 현재 스레드가 대상 스레드를 수정할 수 있는 권한이 없으면 SecurityException이 발생한다.즉, 인터럽트 신호를 줄 수 있는 권한에 대한 검증이다.
  • 대상 스레드가 Object.wait(), Thread.sleep(), Thread.join() 등의 메서드에 의해 blocking이 된 경우 Interrupt state가 clear되고 InterruptException이 발생한다. 추가적으로 Future.get()BlockingQueue.take()도 해당 케이스에 해당한다.
  • 대상 스레드가 InterruptiblaeChannel을 이용하여 IO 연산에 의해 blocking 되었을 경우 channel이 close 된 뒤 Interrupt state가 설정되고 ClosedByInterruptException이 발생한다.
  • 대상 쓰레드가 Selector에서 blocking이 되면 Interrupt state가 설정되고 selection 작업에서 리턴된다.
  • 이 외의 경우에는 interrupt state가 설정된다.

위 예제 코드의 경우 마지막 경우에 의해 interrupt state가 변경되었고 변경된 state에 대해 별다른 처리를 하지 않았다.즉, interrupt state는 대상 스레드가 자신이 인터럽트 된 상태인지 확인할 수 있는 값이다. 해당 값의 경우 Thread.isInterrupted()Thread.interrupted() 두가지 메서드로 확인이 가능하다. 두 메서드의 차이점은 아래와 같다.

  • Thread.isInterrupted() : 인스턴스 메서드이고 호출 후에도 해당 스레드의 interrupt state가 유지된다.
  • Thread.interrupted() : static 메서드이고 호출 후에 해당 스레드의 interrupt state가 초기화 된다.

위 내용을 요약하자면 특정 스레드에서 인터럽트가 발생했음을 확인할 수 있는 방법은 두가지이다.

  1. InterruptException이 발생
  2. Thread내의 Interrupt State를 메서드를 통해 확인

지금부터는 발생된 인터럽트를 처리하는 방법에 대해 살펴보자.

Interrupt Handling 방안

앞선 예제는 무한히 실행 되는 상태를 가지는 스레드를 직접 만들어 예시로 들어 설명하였다.

스레드의 경우 크게 두가지 방식에 의해 생성될 수 있다.

  1. 앞선 예제처럼 직접 run() 메서드를 구현하여 생성하는 경우
  2. Executor(Thread pool)을 사용하는 경우

사실상 두 방법 모두 Runnable 인터페이스를 구현하는것은 마찬가지이다. 아래의 예제를 통해 인터럽트 처리를 위한 run() 메서드 구현을 알아보자. 무한히 어떠한 로직을 수행해야하는 작업을 스레드풀에서 실행해야할 경우 아래와 같은 Task를 submit()을 틍해 실행 가능하다.

public class NewTask extends Runnable {
    @Override
    public void run() {
      while(true) {
		//doSomethig...
		}
    }
  }

다만 위와 같은 스레드는 submit의 리턴 값으로 받은 Future.cancel()을 호출하더라도 정상적으로 종료 되지 않는다. 왜냐하면 cancel() 메서드의 경우도 앞서 배운 인터럽트의 방식을 따르기 때문이다.즉, 종료시키려는 스레드가 인터럽트를 처리하지 않으면 종료될 수 없다.

따라서 우리는 위와 같은 코드를 아래와 같이 인터럽트를 처리하도록 변경해줘야한다.

 public class NewTask extends Runnable {
    @Override
    public void run() {
      while(!Thread.currentThread().isInterrupted()) {
		//doSomethig...
	  }
    }
  }

변경된 로직의 경우 while loop을 돌면서 현재 스레드의 interrupt state를 확인하기 때문에 인터럽트 신호가 들어올 경우 정상적으로 Loop를 탈출할 수 있다.

다음과 같이 while문 내에서 sleep()으로 인해 스레드가 blocking되는 경우는 인터럽트를 어떻게 처리해야할까? sleep의 경우 공식문서에 의하면 Interrupt state가 clear되고 InterruptException이 발생하는 경우이다. 즉, 현재 while문의 조건으로 반복문을 탈출 할 수 없게 된다. 아래의 예제가 이와 같은 상황이다.

 public static void main(String[] args) {
    Thread target = new TargetThread();

    target.start();
    try {
      Thread.sleep(1000);
      target.interrupt(); // 외부에서의 인터럽트 요청
    } catch (InterruptedException e) {
      System.out.println("finished");
    } catch (RuntimeException e) {
      System.out.println("e = " + e);
    }
  }

  static class TargetThread extends Thread {
    @Override
    public void run() throws RuntimeException {
      while (!Thread.currentThread().isInterrupted()) {
        try {
          System.out.println("sleeping...");
          Thread.sleep(100);
        } catch (InterruptedException e) {
          System.out.println("Interrupted!");
        }
      }
    }
  }

위 코드를 실행시켜보면 “sleeping…”이 무한히 콘솔에 출력되다가 1초정도 지난후 “Interrupted!”가 출력되고 이후 무한히 “sleeping…”이 출력된다.

위와 같은 상황은 sleep이라는 메서드에 의해 Thread가 Blocking 되었을 때 인터럽트 신호를 받으면 Interrupt state가 clear되기 때문이다. 즉, while의 조건문을 통해 검사할 state가 초기화 되어 버린것이다.그러면 이와 같은 문제는 어떻게 해결할 수 있을까?

아래와 같이 현재 스레드에 직접 flag를 설정해주면 된다.

static class TargetThread extends Thread {
    @Override
    public void run() throws RuntimeException {
      while (!Thread.currentThread().isInterrupted()) {
        try {
          System.out.println("sleeping...");
 		  //Future.get(), BlockingQueue.take() 등
          Thread.sleep(100);
        } catch (InterruptedException e) {
          System.out.println("Interrupted!");
	      Thread.currentThread().interrupt();
        }
      }
    }

catch문 내에서 InterruptedException이 발생했을 때 현재 스레드 자신의 flag를 interrupt() 메서드를 통해 변경시킨다. flag가 변경 되었으니 while문의 조건을 통해 인터럽트 요청 감지가 가능해진다. 따라서 무한히 출력되는 문제가 해결된다.

추가적으로 Try 블럭내의 로직에는 blocking 가능성이 있는 메서드들인 Future.get(), BlockingQueue.take() 등이 오더라도 동일한 방식으로 해당 문제를 해결할 수 있다.

정리해보면 java에서의 인터럽트는 스레드를 종료하기 위한 방안이고 해당 인터럽트 요청이 들어올 경우 스레드 또는 스레드 풀의 runnable 구현을 적절하게 처리하도록 구현해줘야한다. 관용구처럼 사용되는 Thread.currentThread().interrupt();를 통해 구현해줘도 적절한 처리가 가능하다.