본문 바로가기
java

Thread(2) - 스레드의 상태

by CodingMasterLSW 2025. 1. 15.

스레드의 상태는 다음과 같다.

 

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED

각각의 상태에 대해서 예시코드를 살펴보며 이해해보자.

 

New

public class Example {

    public static void main(String[] args) {
        Thread thread = new Thread(new ExampleTask());
        System.out.println(thread.getState());
        thread.start();
        System.out.println(thread.getState());
    }

    static class ExampleTask implements Runnable {

        @Override
        public void run() {
            System.out.println("예시 테스트 실행 시작");
        }
    }

}

 

이 코드의 결과는 다음과 같다.

New
Runnable
예시 테스트 실행 시작

 

왜 이런 결과가 나오는지 자세하게 파헤쳐보자.

 

우선, NEW의 상태부터 알아보자.

Thread thread = new Thread(new ExampleTask());

 

Thread를 생성하고 작업을 넣는다. 다만, start() 메서드를 호출하기 전의 상태이다.

 

즉, 스레드는 생성되었지만, 생성만 되고 아무런 작업이 할당되지 않은 상태를 NEW라고 한다.

 

thread.start();

 

task-1 스레드에 작업을 할당하는 과정이다. 이 과정에서 task-1 스레드에 run() 메서드가 스택으로 쌓인다. 그림으로 표현하자면, 아래와 같은 형태다.

 

 

이 시점에서 task-1 스레드의 상태는 RUNNABLE이다.

 

RUNNABLE 상태란 스레드가 실행 가능 상태에 있고, CPU에서 실행될 준비가 된 상태를 의미한다. 하지만 실제로 CPU를 사용하는 중인지, 실행 대기 중인지는 알 수 없다. 즉, RUNNABLE 상태는 작업을 실행하기 위해 준비가 완료된 상태를 의미한다.

 

 

위의 그림으로 Runnable에 대해 이해해보자.

 

현재 CPU에서는 A1은 작업중이고, B1, C1은 Scheduling queue에서 작업을 대기하고 있다. 대기 중이지만, B1, C1의 상태는 Runnable이다. 이해가 되지 않는다면, Runnable의 정의를 다시 한 번 떠올리면 좋을 것 같다. 실행 중인 상태가 아닌, 실행 가능한 상태에 있고, CPU에서 실행될 준비가 된 상태가 RUNNABLE이다.

 

BLOCKED

Synchronize와 Lock에 대한 개념을 알아야 하기 때문에, 이후 포스팅에서 설명할 예정이다.

 

WAITING

Thread.join(), Object.wait() 와 같은 함수를 호출 할 때 waiting 상태가 된다. 또는 Lock 획득을 대기하는 과정에서 waiting 상태가 되는데, 이는 추후에 다룰 예정이다.

 

Waiting을 이해하기 위해 왜 waiting이 필요한지, Thread.join()은 어떤 메서드인지 간단하게 알아보자.

 

우선 저번 포스팅에서 예시로 들었던 1~100까지의 덧셈 계산을 두 개의 스레드로 나누어 계산하는 코드를 다른 방법으로 살펴보자.

public class Example {

    public static void main(String[] args) throws InterruptedException {
    
        ExampleTask exampleTask1 = new ExampleTask(1, 50);
        ExampleTask exampleTask2 = new ExampleTask(51, 100);

        Thread thread1 = new Thread(exampleTask1);
        Thread thread2 = new Thread(exampleTask2);
        thread1.start();
        thread2.start();

        System.out.println("task1 result = " + exampleTask1.calSum);
        System.out.println("task2 result = " + exampleTask2.calSum);
        System.out.println("total result = " + (exampleTask1.calSum + exampleTask2.calSum));
    }

    static class ExampleTask implements Runnable {

        private final int startNum;
        private final int endNum;
        private int calSum;

        public ExampleTask(int startNum, int endNum) {
            this.startNum = startNum;
            this.endNum = endNum;
        }

        @Override
        public void run()  {
            int tmpSum = 0;
            sleep(2000);
            for (int i=startNum; i<=endNum; i++) {
                tmpSum += i;
            }
            calSum = tmpSum;
        }
        
    }

}

 

sleep(2000) 은 스레드를 2초동안 기다리는 메서드이다.

 

해당 코드를 실행하면 다음과 같은 결과값이 나온다.

task1 result = 0
task1 result = 0
total result = 0

 

우리가 생각한 결과값은 이게 아니다. 왜 전부 0이란 결과값이 나왔을까? 그림을 통해 알아보자.

 

ExampleTask exampleTask1 = new ExampleTask(1, 50);
ExampleTask exampleTask2 = new ExampleTask(51, 100);
Thread thread1 = new Thread(exampleTask1);
Thread thread2 = new Thread(exampleTask2);

 

 

상단의 코드를 실행시켰을 때의 모습이다. 이 시점에서 Thread-1, Thread-2의 상태는 Runnable이다.

 

thread1.start();
thread2.start();

 

이 부분을 주의깊게 봐야한다. 

 

main 스레드의 주된 역할은 명령이다. Thread-1, Thread-2를 생성하고, start() 하라는 명령을 내린다. 다만, 메인스레드는 명령을 내린후에 기다려주지 않는다. 명령을 내리면 이후 명령을 받은 스레드들이 바로 작업을 진행하고, 메인 스레드는 본인의 역할을 마저 하러 간다. 즉, thread1.start() 메서드 호출 후 바로 thread2.start() 메서드를 호출한다는 것이다. 이후에 메인 스레드의 역할은 print() 문을 출력하는 것이다.

System.out.println("task1 result = " + exampleTask1.calSum);
System.out.println("task1 result = " + exampleTask2.calSum);
System.out.println("total result = " + (exampleTask1.calSum + exampleTask2.calSum));

 

 

각 스레드의 작업인 run()을 살펴보자. 

@Override
public void run()  {
   int tmpSum = 0;
   sleep(2000);
   for (int i=startNum; i<=endNum; i++) {
        tmpSum += i;
   }
   calSum = tmpSum;
}

 

각각의 스레드들은 작업을 2초간의 휴식기를 가진 후 작업을 수행한다. 그리고 main 스레드는 Thread-1, Thread-2의 작업을 기다리지 않고 이후의 출력문을 수행한다. 시간의 흐름을 그림으로 그려보자면

 

뭐... 현재는 이런 상황이다. 아직 작업이 완료가 되지 않았고, main 스레드에서는 결과값을 출력해버리니 0이라는 값이 나왔다는것이다. 그러면 sleep(3000) 빼면 되지 않나? 생각할 수 있는데, 예시를 위해 sleep(3000)을 적용한거다. 

 

1~100이 아니라, 1~ 2_100_000_000 까지의 계산이라면? 또는 그 이상이라면? 아니면 CPU 작업이 오래걸리는 작업이라면? 위와 같은 오류는 또 발생한다. 결국, sleep이 없어도 해결해야하는 문제다. 이는 Thread.join()으로 해결할 수 있다.

 

 

 

 

변경된 코드

    public static void main(String[] args) throws InterruptedException {
        ExampleTask exampleTask1 = new ExampleTask(1, 50);
        ExampleTask exampleTask2 = new ExampleTask(51, 100);
        Thread thread1 = new Thread(exampleTask1);
        Thread thread2 = new Thread(exampleTask2);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("task1 result = " + exampleTask1.calSum);
        System.out.println("task1 result = " + exampleTask2.calSum);
        System.out.println("total result = " + (exampleTask1.calSum + exampleTask2.calSum));
    }

 

join()을 통해 대기를 시도하면, 해당 스레드의 작업이 끝날 때 까지 기다린다. 즉, 이 시점에서 main 스레드의 상태는 WAITING 상태이다. 이후 Thread-1, Thread-2의 작업이 끝난 이후에 calSum을 출력하기에 우리가 예상했던 결과값이 나온다.

 

 

TIMED_WAITING

 

waiting과 비슷하지만, 일정 시간동안만 기다리는 상태이다. 위의 예시에서 본 sleep(ms) 를 실행했을 때, 일정 시간동안 대기하기에 waiting이 아닌 timed_waiting상태이고, join또한 매개변수에 대기 시간을 넣어주면 timed_waiting 상태가 된다.

TERMINATED

스레드가 종료된 상태를 의미한다. 간단하지만 코드를 통해 살펴보자면

public class Example {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ExampleTask(), "task-1");
        thread.start();
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }

    static class ExampleTask implements Runnable {

        @Override
        public void run() {
            System.out.println("메서드 호출");
        }
    }

}

 

task-1 스레드가 종료되는 것을 출력하기 위해 메인 스레드를 잠시 timed_waiting으로 대기 후,  이후에 thread의 상태를 출력해보았다.

메서드 호출
TERMINATED

 

terminated는 종료된 스레드의 상태를 보여주는 것이기에 매우 간단하다.


멀티스레드는 필요한 기능이지만, 충분히 이해 못 하고 사용할 경우 동시성 문제가 발생할 수 있다. 다음 포스팅에서는 동시성 문제, Synchronize, lock, block에 대해 포스팅 하려고 한다. 블로그 글을 보는 것 또한 좋지만, 인프런의 김영한님의 자바 고급 1편 강의를 듣는것을 매우 추천한다.