스레드는 생성되고 실행되며 종료되는 일련의 생명주기를 갖는다. 자바에서는 Thread.State
열거형으로 상태를 구분한다.
상태 | 설명 |
---|---|
NEW | 스레드 객체가 생성되었으나 start() 가 호출되지 않은 상태 |
RUNNABLE | 실행 준비 완료 상태. 실제로 CPU에 의해 실행될 수도 있음 |
BLOCKED | 동기화 락을 얻기 위해 대기 중인 상태 |
WAITING | 무기한 대기 상태 (다른 스레드가 notify해야 깨어남) |
TIMED_WAITING | 일정 시간 동안만 대기 (sleep , join(timeout) 등) |
TERMINATED | 스레드 실행 종료 상태 |
NEW -> RUNNABLE
: start()
호출RUNNABLE -> TIMED_WAITING
: sleep()
, join(timeout)
등 호출TIMED_WAITING -> RUNNABLE
: 시간이 지나거나 notify 등으로 깨어남RUNNABLE -> TERMINATED
: run()
메서드 종료public class ThreadStateMain {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable(), "myThread");
log.info("myThread.state1 = " + thread.getState()); // NEW
log.info("myThread.start()");
thread.start();
Thread.sleep(1000);
log.info("myThread.state3 = " + thread.getState()); // TIMED_WAITING
Thread.sleep(4000);
log.info("myThread.state5 = " + thread.getState()); // TERMINATED
log.info("end");
}
static class MyRunnable implements Runnable {
@Override
public void run() {
try {
log.info("start");
log.info("myThread.state2 = " + Thread.currentThread().getState()); // RUNNABLE
log.info("sleep() start");
Thread.sleep(3000);
log.info("sleep() end");
log.info("myThread.state4 = " + Thread.currentThread().getState()); // RUNNABLE
log.info("end");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
Thread.currentThread()
를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.Thread.sleep()
: 해당 코드를 호출한 스레드는 TIMED_WAITING
상태가 되면서 특정 시간 만큼 대기한다. 시간은 밀리초(ms) 단위이다. 1밀리초 = 1/1000 초, 1000밀리초 = 1초이다.Thread.sleep()
은 InterruptedException
이라는 체크 예외를 던진다. 따라서 체크 예외를 잡아서 처run()
메서드 안에서는 체크 예외를 반드시 잡아야 한다.InterruptedException
은 인터럽트가 걸릴 때 발생하는데, 인터럽트는 뒤에서 알아본다. 지금은 체크 예외가 발생한다 정도만 이해하면 충분하다.11:40:31.503 [ main] myThread.state1 = NEW
11:40:31.505 [ main] myThread.start()
11:40:31.505 [ myThread] start
11:40:31.505 [ myThread] myThread.state2 = RUNNABLE
11:40:31.505 [ myThread] sleep() start
11:40:32.507 [ main] myThread.state3 = TIMED_WAITING
11:40:34.510 [ myThread] sleep() end
11:40:34.512 [ myThread] myThread.state4 = RUNNABLE
11:40:34.512 [ myThread] end
11:40:36.511 [ main] myThread.state5 = TERMINATED
11:40:36.512 [ main] end
main
스레드를 통해 myThread
객체를 생성한다. 스레드 객체만 생성하고 아직 start()
를 호출하지 않았기 때문에 NEW 상태이다.myThread.start()
를 호출해서 myThread
를 실행 상태로 만든다. 따라서 RUNNABLE
상태가 된다. 참고로 실행 상태가 너무 빨리 지나가기 때문에 main
스레드에서 myThread
의 상태를 확인하기는 어렵다. 대신에 자기 자신인 myThread
에서 실행 중인 자신의 상태를 확인했다.Thread.sleep(3000)
: 해당 코드를 호출한 스레드는 3000ms (3초)간 대기한다. myThread
가 해당코드를 호출했으므로 3초간 대기하면서 TIMED_WAITING
상태로 변한다.main
스레드가 myThread
의 TIMED_WAITING
상태를 확인하기 위해 1초간 대기하고 상태를 확인했다.myThread
는 3초의 시간 대기 후 TIMED_WAITING
상태에서 빠져나와 다시 실행될 수 있는RUNNABLE
상태로 바뀐다. myThread
가 run()
메서드를 실행 종료하고 나면 TERMINATED
상태가 된다.myThread
입장에서 run()
이 스택에 남은 마지막 메서드인데 run()
까지 실행되고 나면 스택이 완전히 비워진다. 이렇게 스택이 비워지면 해당 스택을 사용하는 스레드도 종료된다.Runnable
인터페이스의 run()
메서드를 구현할 때 InterruptedException
체크 예외를 throws로 밖에 선언할 수 없다
Runnable
인터페이스
public interface Runnable {
void run();
}
자바에서 메서드를 재정의 할 때 재정의 메서드가 지켜야할 예외와 관련된 규칙이 있다.
Runnable
인터페이스의 run()
메서드는 아무런 체크 예외를 던지지 않는다. 따라서 Runnable
인터페이스의 run()
메서드를 재정의 하는 곳에서는 체크 예외를 밖으로 던질 수 없다. 다음 코드를 실행하면 컴파일 오류가 발생한다.
static class MyRunnable implements Runnable {
public void run() throws InterruptedException { // 컴파일 오류
Thread.sleep(3000);
}
}
왜냐하면 InterruptedException
은 체크 예외이기 때문에 반드시 try-catch
블록으로 감싸거나 던져야 한다. 하지만 run()
메서드는 예외를 throws로 던질 수 없기 때문에 반드시 try-catch
로 내부에서 처리해야 한다
부모 클래스의 메서드를 호출하는 클라이언트 코드는 부모 메서드가 던지는 특정 예외만을 처리하도록 작성된다. 자식 클래스가 상위 타입의 예외를 던지면 해당 코드는 모든 예외를 제대로 처리하지 못할 수 있다. 이는 예외 처리의 일관성을 해치고 예상하지 못한 런타임 오류를 초래할 수 있다.
체크 예외를 run()
메서드에서 던질 수 없도록 강제함으로써 개발자는 반드시 체크 예외를 try-catch
블록 내에서 처리하게 된다. 이는 예외 발생 시 예외가 적절히 처리되지 않아서 프로그램이 비정상 종료되는 상황을 방지할 수 있다. 특히 멀티스레딩 환경에서는 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지할 수 있다.
이번 학습을 통해 자바 스레드의 생명 주기가 어떻게 전이되는지를 코드와 로그를 통해 명확히 이해할 수 있었다. 특히 NEW
, RUNNABLE
, TIMED_WAITING
, TERMINATED
등 상태들이 어떻게 전이되며 그 시점을 정확히 포착하는 것이 멀티스레드 디버깅에 중요한 역할을 한다는 걸 실감했다. sleep()
메서드로 인해 TIMED_WAITING
상태에 들어가는 흐름을 직접 확인하면서 상태 변화에 대한 공부를 할 수 있었다.
또한 Runnable
인터페이스의 run()
메서드에서 InterruptedException
같은 체크 예외를 밖으로 던질 수 없다는 자바의 예외 처리 규칙은 멀티스레드 환경에서의 안정성을 위한 설계임을 이해하게 되었다. 덕분에 자바가 왜 run()
메서드 내에서 반드시 try-catch
처리를 강제하는지 그 이유에 대해 구조적 측면에서 접근할 수 있었다. 이런 규칙을 알고 쓰는 것과 모르고 쓰는 것은 큰 차이를 만든다는 걸 다시 한 번 느꼈다.
참고