본문 바로가기

Java

[JAVA] 스레드 제어와 생명 주기

Thread 클래스

Thread 클래스는 스레드를 생성하고 관리하는 기능을 제공한다.

Thread 클래스가 제공하는 메서드를 이용해 main 스레드의 정보들을 확인해보자.

Thread 클래스의 toString 오버라이딩

1. 스레드 객체 정보

Thread mainThread = Thread.currentThread();
System.out.println("mainThread = " + mainThread);
  • Thread 클래스의 toString() 메서드는 스레드 ID, 스레드 이름, 우선순위, 스레드 그룹을 포함하는 문자열을 반환한다.
  • 출력: Thread[#1,main,5,main]

2. 스레드 ID

System.out.println("mainThread.getId() = " + mainThread.getId());
  • threadId(): 스레드의 고유 식별자를 반환하는 메서드이다.
  • 이 ID는 JVM 내에서 각 스레드에 대해 유일하다.
  • ID는 스레드가 생성될 때 할당되며, 직접 지정할 수 없다.

3. 스레드 이름

System.out.println("mainThread.getName() = " + mainThread.getName());
  • getName(): 스레드의 이름을 반환하는 메서드이다.
  • 참고로 스레드 ID는 중복되지 않지만, 스레드 이름은 중복될 수 있다.

4. 스레드 우선순위

System.out.println("mainThread.getPriority() = " + mainThread.getPriority());
  • getPriority(): 스레드의 우선순위를 반환하는 메서드이다.
  • 우선순위는 1 (가장 낮음)에서 10 (가장 높음)까지의 값으로 설정할 수 있으며, 기본값은 5이다.
  • setPriority() 메서드를 사용해서 우선순위를 변경할 수 있다.
  • 우선순위는 스레드 스케줄러가 어떤 스레드를 우선 실행할지 결정하는 데 사용된다.
  • 하지만 실제 실행 순서는 JVM 구현과 운영체제에 따라 달라질 수 있다.

5. 스레드 그룹

System.out.println("mainThread.getThreadGroup() = " + mainThread.getThreadGroup());
  • getThreadGroup(): 스레드가 속한 스레드 그룹을 반환하는 메서드이다.
  • 스레드 그룹은 스레드를 그룹화하여 관리할 수 있는 기능을 제공한다.
  • 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속하게 된다.
  • 스레드 그룹은 여러 스레드를 하나의 그룹으로 묶어서 특정 작업(예: 일괄 종료, 우선순위 설정 등)을 수행할 수 있 다.

💡 부모 스레드(Parent Thread)

새로운 스레드를 생성하는 스레드를 의미한다.
스레드는 기본적으로 다른 스레드에 의해 생성된다.
이러한 생성 관계에서 새로 생성된 스레드는 생성한 스레드를 부모로 간주한다.

예를 들어 main 메서드에서 만든 myThreadmain 스레드에 의해 생성되었으므로 main 스레드가 부모 스레드이다.

 

6. 스레드 상태

System.out.println("mainThread.getState() = " + mainThread.getState());
  • getState(): 스레드의 현재 상태를 반환하는 메서드이다.
  • 반환되는 값은 Thread.State 열거형에 정의된 상수 중 하나이다.

🌟 주요 상태

  • NEW: 스레드가 아직 시작되지 않은 상태이다.
  • RUNNABLE: 스레드가 실행 중이거나 실행될 준비가 된 상태이다.
  • BLOCKED: 스레드가 동기화 락을 기다리는 상태이다.
  • WAITING: 스레드가 다른 스레드의 특정 작업이 완료되기를 기다리는 상태이다.
  • TIMED_WAITING: 일정 시간 동안 기다리는 상태이다.
  • TERMINATED: 스레드가 실행을 마친 상태이다.

스레드의 생명 주기

스레드는 생성하고 시작하고, 종료되는 생명주기를 가진다. 스레드의 생명 주기에 대해 자세히 알아보자.

스레드 생명 주기

자바 스레드의 생명 주기는 여러 상태(state)로 나뉘어지며, 각 상태는 스레드가 실행되고 종료되기까지의 과정을 나타낸다.
자바 스레드의 생명 주기를 자세히 알아보자.

 

1. New (새로운 상태)

  • 스레드가 생성되고 아직 시작되지 않은 상태이다.
  • 이 상태에서는 Thread 객체가 생성되지만, start() 메서드가 호출되지 않은 상태이다.
  • 예: Thread thread = new Thread(runnable);

2. Runnable (실행 가능 상태)

  • 스레드가 실행될 준비가 된 상태이다. 이 상태에서 스레드는 실제로 CPU에서 실행될 수 있다.
  • start() 메서드가 호출되면 스레드는 이 상태로 들어간다.
  • 예: thread.start();

이 상태는 스레드가 실행될 준비가 되어 있음을 나타내며, 실제로 CPU에서 실행될 수 있는 상태이다. 그러나 Runnable 상태에 있는 모든 스레드가 동시에 실행되는 것은 아니다. 운영체제의 스케줄러가 각 스레드에 CPU 시간을 할당하여 실행하기 때문에, Runnable 상태에 있는 스레드는 스케줄러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행된다. 참고로 운영체제 스케줄러의 실행 대기열에 있든, CPU에서 실제 실행되고 있든 모두 RUNNABLE 상태이다. 자바에서 둘을 구분해서 확인할 수는 없다. 보통 실행 상태라고 부른다.

 

3. Blocked (차단 상태)

  • 스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태이다.
  • 예를 들어, synchronized 블록에 진입하기 위해 락을 얻어야 하는 경우 이 상태에 들어간다.
  • 예: synchronized (lock) { ... } 코드 블록에 진입하려고 할 때, 다른 스레드가 이미 lock 의 락을 가지고 있는 경우.

4. Waiting (대기 상태)

  • 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
  • wait() , join() 메서드가 호출될 때 이 상태가 된다.
  • 스레드는 다른 스레드가 notify() 또는 notifyAll() 메서드를 호출하거나, join() 이 완료될 때까 지 기다린다.
  • 예: object.wait();

5. Timed Waiting (시간 제한 대기 상태)

  • 스레드가 특정 시간 동안 다른 스레드의 작업이 완료되기를 기다리는 상태이다.
  • sleep(long millis) , wait(long timeout) , join(long millis) 메서드가 호출될 때 이 상태가 된다.
  • 주어진 시간이 경과하거나 다른 스레드가 해당 스레드를 깨우면 이 상태에서 벗어난다.
  • 예: Thread.sleep(1000);

6. Terminated (종료 상태)

  • 스레드의 실행이 완료된 상태이다.
  • 스레드가 정상적으로 종료되거나, 예외가 발생하여 종료된 경우 이 상태로 들어간다.
  • 스레드는 한 번 종료되면 다시 시작할 수 없다.

자바 스레드의 상태 전이 과정

  1. New → Runnable: start() 메서드를 호출하면 스레드가 Runnable 상태로 전이된다.
  2. Runnable → Blocked/Waiting/Timed Waiting: 스레드가 락을 얻지 못하거나, wait()또는 sleep() 메서드를 호출할 때 해당 상태로 전이된다.
  3. Blocked/Waiting/Timed Waiting → Runnable: 스레드가 락을 얻거나, 기다림이 완료되면 다시 Runnable 상태로 돌아간다.
  4. Runnable → Terminated: 스레드의 run() 메서드가 완료되면 스레드는 Terminated 상태가 된다.

Join( )

main 스레드에서 다음 코드를 실행하게 되면 main 스레드는 thread-1 , thread-2 가 종료될 때 까지 기다린다.

 main 스레드는 WAITING 상태가 된다.

thread1.join();
thread2.join();
public class JoinMainV3 {

    public static void main(String[] args) throws InterruptedException {
        log("Start");
        SumTask task1 = new SumTask(1, 50);
        SumTask task2 = new SumTask(51, 100);

        Thread thread1 = new Thread(task1, "thread-1");
        Thread thread2 = new Thread(task2, "thread-2");

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

        // 스레드가 종료될 때까지 대기
        log("join() - main 스레드가 thread1, thread2 종료까지 대기");
        thread1.join();
        thread2.join();
        log("main 스레드 대기 완료");

        log("task1.result = " + task1.result);
        log("task2.result = " + task2.result);

        int sumAll = task1.result + task2.result;
        log("task1 + task2 = " + sumAll);
        log("End");
    }

    static class SumTask implements Runnable {
        int startValue;
        int endValue;
        int result = 0;

        SumTask(int startValue, int endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }

        @Override
        public void run() {
            log("작업 시작");
            int sum = 0;
            sleep(2000);
            for (int i = startValue; i <= endValue; i++) {
                sum += i;
            }
            result = sum;
            log("작업 완료 result = " + result);
        }
    }
}

JoinMainV3의 시간별 스레드 상태 변화

예를 들어서 thread-1 이 아직 종료되지 않았다면 main 스레드는 thread1.join() 코드 안에서 더는 진행하지 않고 멈추어 기다린다. 이후에 thread-1 이 종료되면 main 스레드는 RUNNABLE 상태가 되고 다음 코드로 이동한다.

이때 thread-2이 아직 종료되지 않았다면 main 스레드는 thread2.join() 코드 안에서 진행하지 않고 멈추어 기다린다.

이후에 thread-2 이 종료되면 main 스레드는 RUNNABLE 상태가 되고 다음 코드로 이동한다.

이 경우 thread-1 이 종료되는 시점에 thread-2 도 거의 같이 종료되기 때문에 thread2.join() 은 대기하지 않고 바로 빠져나온다.

 

🌟 Waiting (대기 상태)

스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
join() 을 호출하는 스레드는 대상 스레드가 TERMINATED 상태가 될 때 까지 대기한다.
대상 스레드가 TERMINATED 상태가 되면 호출 스레드는 다시 RUNNABLE 상태가 되면서 다음 코드를 수행한다.

 

즉, main 스레드는 thread1.join()을 만나 WAITING 상태가 되었다가 thread-1이 종료되면 RUNNABLE 상태가 되고

밑으로 내려가 thread2.join()을 만나 다시 thread-2가 종료될 때까지 WAITING 상태가 되어있는 것이다.

마찬가지로 thread-2가 종료된다면 다시 RUNNABLE 상태가 되어 코드 밑을 수행하게 된다

 

이렇듯 특정 스레드가 완료될 때 까지 기다려야 하는 상황이라면 join() 을 사용하면 된다.

하지만 join()의 단점은 다른 스레드가 완료될 때까지 무기한 기다리는 단점이 있다.

만약 다른 스레드의 작업을 일정 시간 동안만 기다리고 싶다면 어떻게 해야할까?

 

join() 은 두 가지 메서드가 있다.

thread.join(); // 무한정 대기
thread.join(1000); // 특정 시간 만큼만 대기
  • join() : 호출 스레드는 대상 스레드가 완료될 때 까지 무한정 대기함.
  • join(ms) : 호출 스레드는 특정 시간 만큼만 대기한다. 지정한 시간이 지나면 다시 RUNNABLE 상태가 되면서 다음 코드를 수행함.
  • 이때 main 스레드의 상태는 WAITING 이 아니라 TIMED_WAITING 이 된다.
  • 보통 무기한 대기하면 WAITING 상태가 되고특정 시간 만큼만 대기하는 경우 TIMED_WAITING 상태가 된다.

다른 스레드가 끝날 때 까지 무한정 기다려야 한다면 join() 을 사용하고, 다른 스레드의 작업을 무한정 기다릴 수 없다면 join(ms) 를 사용하면 된다. 물론 기다리다 중간에 나오는 상황인데, 결과가 없다면 추가적인 오류 처리가 필요할 수 있다.