[Java] 35. 스레드 제어와 생명 주기(1)

2025. 7. 1. 16:04·Java/멀티스레드&동시성
스레드 제어와 생명 주기(1)

스레드 제어와 생명 주기(1)

#Java/멀티스레드

정리

스레드 기본 정보

Thread 클래스는 스레드를 생성하고 관리하는 기능을 제공한다. Thread 클래스가 제공하는 정보들을 직접 확인할 수 있다.


0. 스레드 조회

Thread mainThread = Thread.currentThread();

현재 실행중인 스레드 객체를 가져온다.


1. 스레드 생성
스레드를 생성할 때는 실행할 Runnable 인터페이스의 구현체와, 스레드의 이름을 전달할 수 있다.


Thread myThread = new Thread(new HelloRunnable(), "myThread");

  • Runnable 인터페이스: 실행할 작업을 포함하는 인터페이스이다. HelloRunnable 클래스는 Runnable 인터페이스를 구현한 클래스이다.
  • 스레드 이름: "myThread" 라는 이름으로 스레드를 생성한다. 이 이름은 디버깅이나 로깅 목적으로 유용하다. 참고로 이름을 생략하면 Thread-0 , Thread-1 과 같은 임의의 이름이 생성된다.


2. 스레드 객체 정보

log("myThread = " + myThread);

  • myThread 객체를 문자열로 변환하여 출력한다. Thread 클래스의 toString() 메서드는 스레드 ID, 스레드 이름, 우선순위, 스레드 그룹을 포함하는 문자열을 반환한다.
  • Thread[#21,myThread,5,main]

3. 스레드 ID


log("myThread.threadId() = " + myThread.threadId());

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

4. 스레드 이름


log("myThread.getName() = " + myThread.getName());

  • getName(): 스레드의 이름을 반환하는 메서드이다. 생성자에서 "myThread" 라는 이름을 지정했기 때문에, 이 값이 반환된다. 참고로 스레드 ID는 중복되지 않지만, 스레드 이름은 중복될 수 있다.

5. 스레드 우선순위


log("myThread.getPriority() = " + myThread.getPriority());
  • getPriority(): 스레드의 우선순위를 반환하는 메서드이다. 우선순위는 1 (가장 낮음)에서 10 (가장 높음)까지의 값으로 설정할 수 있으며, 기본값은 5이다. setPriority() 메서드를 사용해서 우선순위를 변경할 수 있다.
    • 우선순위가 높을수록 더 많이 실행된다. 운영체제 스케줄러에게 힌트를 주는거다.
    • 근데 운영체제가 알잘딱으로 해주기 때문에, 직접 우선순위를 지정할 일은 거의 없다.
  • 우선순위는 스레드 스케줄러가 어떤 스레드를 우선 실행할지 결정하는 데 사용된다. 하지만 실제 실행 순서는 JVM 구현과 운영체제에 따라 달라질 수 있다.
  • CPU가 2개이고 스레드가 2개이면 우선순위가 크게 상관이 없지만, CPU가 적고, Thread가 엄청 많으면 우선순위가 높은 스레드가 더 자주 실행된다.

6. 스레드 그룹

log("myThread.getThreadGroup() = " + myThread.getThreadGroup());
  • getThreadGroup(): 스레드가 속한 스레드 그룹을 반환하는 메서드이다. 스레드 그룹은 스레드를 그룹화하여 관리할 수 있는 기능을 제공한다. 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속하게 된다.
  • 스레드 그룹은 여러 스레드를 하나의 그룹으로 묶어서 특정 작업(예: 일괄 종료, 우선순위 설정 등)을 수행할 수 있다.
  • 부모 스레드(Parent Thread): 새로운 스레드를 생성하는 스레드를 의미한다. 스레드는 기본적으로 다른 스레드에 의해 생성된다. 이러한 생성 관계에서 새로 생성된 스레드는 생성한 스레드를 부모로 간주한다. 예를 들어 myThread 는 main 스레드에 의해 생성되었으므로 main 스레드가 부모 스레드이다.
  • main 스레드는 기본으로 제공되는 main 스레드 그룹에 소속되어 있다. 따라서 myThread 도 부모 스레드인 main 스레드의 그룹인 main 스레드 그룹에 소속된다.
  • 스레드 그룹 기능은 직접적으로 잘 사용하지는 않는다.

7. 스레드 상태


log("myThread.getState() = " + myThread.getState());

  • getState(): 스레드의 현재 상태를 반환하는 메서드이다. 반환되는 값은 Thread.State 열거형에 정의된 상수 중 하나이다. 주요 상태는 다음과 같다.
    • NEW: 스레드가 아직 시작되지 않은 상태이다. (생성만 한 상태)
      • 아직 thread.start()를 호출하지 않는 상태이다.
    • RUNNABLE: 스레드가 실행 중이거나 실행될 준비가 된 상태이다.
    • BLOCKED: 스레드가 동기화 락을 기다리는 상태이다.
    • WAITING: 스레드가 다른 스레드의 특정 작업이 완료되기를 기다리는 상태이다. (대기)
    • TIMED_WAITING: 일정 시간 동안 기다리는 상태이다.
      • Sleep 을 생각해보자. Sleep 스레드 getState()하면 이 상태이다.
    • TERMINATED: 스레드가 실행을 마친 상태이다.

스레드 생명주기


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


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

  2. Runnable (실행 가능 상태)
    • 스레드가 실행될 준비가 된 상태이다. 이 상태에서 스레드는 실제로 CPU에서 실행될 수 있다.
    • start() 메서드가 호출되면 스레드는 이 상태로 들어간다.
    • 예: thread.start();
    • 이 상태는 스레드가 실행될 준비가 되어 있음을 나타내며, 실제로 CPU에서 실행될 수 있는 상태이다. 그러나 Runnable 상태에 있는 모든 스레드가 동시에 실행되는 것은 아니다. 운영체제의 스케줄러가 각 스레드에 CPU 시간을 할당하여 실행하기 때문에, Runnable 상태에 있는 스레드는 스케줄러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행된다.
    • CPU 1개에 스레드 A.start(), 스레드 B.start()하면 사람이 봤을 때는 동시에 실행되는 것처럼 보이고, 두 개 다 실행되고 있다. 물리적으로 보면, CPU 입장에서는 CPU가 처리하고 있는 스레드가 있고, OS 스케줄러에 의해 대기중인 스레드가 있다. 근데 둘 다 실행 가능한 상태이므로, Runnable 상태라고 부르는거다.
    • 참고로 운영체제 스케줄러의 실행 대기열에 있든, CPU에서 실제 실행되고 있든 모두 RUNNABLE 상태이다. 자바에서 둘을 구분해서 확인할 수는 없다.
    • 보통 실행 상태라고 부른다.
    • Blocked, Wating, Timed Wating의 경우 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 (종료 상태)
    • 스레드의 실행이 완료된 상태이다.
      • run까지 스택 프레임에서 나가버린 상태.
    • 스레드가 정상적으로 종료되거나, 예외가 발생하여 종료된 경우 이 상태로 들어간다.
    • 스레드는 한 번 종료되면 다시 시작할 수 없다.

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

  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 상태가 된다.

package thread.control;
import static util.MyLogger.log;
public class ThreadStateMain {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyRunnable(), "myThread");
        log("myThread.state1 = " + thread.getState()); // NEW
        log("myThread.start()");
        thread.start();
        Thread.sleep(1000);
        log("myThread.state3 = " + thread.getState()); // TIMED_WAITING
        Thread.sleep(4000);
        log("myThread.state5 = " + thread.getState()); // TERMINATED
        log("end");
    }
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            try {
                log("start");
                log("myThread.state2 = " +
                    Thread.currentThread().getState()); // RUNNABLE
                log("sleep() start");
                Thread.sleep(3000);
                log("sleep() end");
                log("myThread.state4 = " +
                    Thread.currentThread().getState()); // RUNNABLE
                log("end");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

  • Thread.sleep() : 해당 코드를 호출한 스레드는 TIMED_WAITING 상태가 되면서 특정 시간 만큼 대기한다.

  • state5 = TERMINATED
    • myThread 가 run() 메서드를 실행 종료하고 나면 TERMINATED 상태가 된다.
    • myThread 입장에서 run() 이 스택에 남은 마지막 메서드인데, run() 까지 실행되고 나면 스택이 완전히 비워진다. 이렇게 스택이 비워지면 해당 스택을 사용하는 스레드도 종료된다.

Runnable - 체크 예외


Runnable 인터페이스는 다음과 같이 정의되어 있다.


public interface Runnable {
    void run();
}

Runnable 인터페이스의 run() 메서드는 아무런 체크 예외를 던지지 않는다.
따라서 Runnable 인터페이스의 run() 메서드를 재정의 하는 곳에서는 체크 예외를 밖으로 던질 수 없다.


복습: 체크 예외 재정의 규칙

  • 자식 클래스에 재정의된 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만을 던질 수 있다.
  • 원래 메서드가 체크 예외를 던지지 않는 경우, 재정의된 메서드도 체크 예외를 던질 수 없다.

안전한 예외 처리
체크 예외를 run() 메서드에서 던질 수 없도록 강제함으로써, 개발자는 반드시 체크 예외를 try-catch 블록 내에서 처리하게 된다. 이는 예외 발생 시 예외가 적절히 처리되지 않아서 프로그램이 비정상 종료되는 상황을 방지할 수 있다. 특히 멀티스레딩 환경에서는 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지할 수 있다.


하지만 체크 예외는 최근에 선호하지 않는 방식이다. 체크 예외를 런타임 예외로 변경하는 간단한 유틸리티를 만들어 사용하자.

public abstract class ThreadUtils {
    public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            log("인터럽트 발생, " + e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

join

join() 메서드를 통해 WAITING (대기 상태)가 무엇이고 왜 필요한지 알아보자.


Waiting (대기 상태)

  • 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.

public class JoinMainV0 {
    public static void main(String[] args) {
        log("Start");
        Thread thread1 = new Thread(new Job(), "thread-1");
        Thread thread2 = new Thread(new Job(), "thread-2");
        thread1.start();
        thread2.start();
        log("End");
    }
    static class Job implements Runnable {
        @Override
        public void run() {
            log("작업 시작");
            sleep(2000);
            log("작업 완료");
        }
    }
}

여기서 thread-1, thread-2 가 종료된 다음에 main 스레드를 가장 마지막에 종료하려면 어떻게 해야할까?
(ex. thread-1에서는 1 ~ 50을 더하고, thread-2에서는 51 ~ 100을 더하고, 최종적으로 main에서는 1 ~ 100을 더한 결과를 출력)


핵심은 main 스레드가 thread-1, thread-2 의 계산이 끝날 때까지 기다려야 한다는 점이다.
특정 스레드를 기다리게 하는 가장 간단한 방법은 sleep() 을 사용하는 것이다.


하지만 이렇게 sleep() 을 사용해서 무작정 기다리는 방법은 대기 시간에 손해도 있고, 또 thread-1, thread-2 의 수행시간이 달라지는 경우에는 정확한 타이밍을 맞추기 어렵다.


더 나은 방법은 thread-1, thread-2 가 계산을 끝내고 종료될 때까지 main 스레드가 기다리는 방법이다.
예를 들어서 main 스레드가 반복문을 사용해서 thread-1, thread-2 의 상태가 TERMINATED 가 될 때까지 계속 확인하는 방법이 있다.


while(thread.getState() != TERMINATED) {
    // 스레드의 상태가 종료될 때 까지 계속 반복
}
// 계산 결과 출력

이런 방법은 번거롭고 또 계속되는 반복문은 CPU 연산을 사용한다. 이때 join() 메서드를 사용하면 깔끔하게 문제를 해결할 수 있다.


package thread.control.join;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
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;
        public SumTask(int startValue, int endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }
        @Override
        public void run() {
            log("작업 시작");
            sleep(2000);
            int sum = 0;
            for (int i = startValue; i <= endValue; i++) {
                sum += i;
            }
            result = sum;
            log("작업 완료 result = " + result);
        }
    }
}


(this: this 는 호출된 인스턴스 메서드가 소속된 객체를 가리키는 참조이며, 이것이 스택 프레임 내부에 저장되어 있다. 컨텍스트 스위칭이 발생할 때 인스턴스 필드에 계속해서 접근할 수 있는 이유이다.)


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


Waiting (대기 상태) : join() 을 호출하는 스레드는 대상 스레드가 TERMINATED 상태가 될 때 까지 대기한다. 대상 스레드가 TERMINATED 상태가 되면 호출 스레드는 다시 RUNNABLE 상태가 되면서 다음 코드를 수행한다.


이렇듯 특정 스레드가 완료될 때 까지 기다려야 하는 상황이라면 join() 을 사용하면 된다.
하지만 join() 의 단점은 다른 스레드가 완료될 때 까지 무기한 기다리는 단점이 있다. 만약 다른 스레드의 작업을 일정 시간 동안만 기다리고 싶다면 어떻게 해야할까?


join - 특정 시간 만큼만 대기

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

  • join() : 호출 스레드는 대상 스레드가 완료될 때 까지 무한정 대기한다.
  • join(ms) : 호출 스레드는 특정 시간 만큼만 대기한다. 호출 스레드는 지정한 시간이 지나면 다시 RUNNABLE 상태가 되면서 다음 코드를 수행한다.

	public static void main(String[] args) throws InterruptedException {
        log("Start");
        SumTask task1 = new SumTask(1, 50);
        Thread thread1 = new Thread(task1, "thread-1");
        thread1.start();
        //스레드가 종료될 때 까지 대기
        log("join(1000) - main 스레드가 thread1 종료까지 1초 대기");
        thread1.join(1000);
        log("main 스레드 대기 완료");
        log("task1.result = " + task1.result);
    }


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




참고) 김영한의 실전 자바 - 고급 1편

'Java > 멀티스레드&동시성' 카테고리의 다른 글

[Java] 38. 동기화 - synchronized  (0) 2025.07.01
[Java] 37. 메모리 가시성  (0) 2025.07.01
[Java] 36. 스레드 제어와 생명 주기(2)  (0) 2025.07.01
[Java] 34. 스레드 생성과 실행  (0) 2025.07.01
[Java] 33. 프로세스와 스레드  (0) 2025.07.01
'Java/멀티스레드&동시성' 카테고리의 다른 글
  • [Java] 37. 메모리 가시성
  • [Java] 36. 스레드 제어와 생명 주기(2)
  • [Java] 34. 스레드 생성과 실행
  • [Java] 33. 프로세스와 스레드
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (463)
      • 개발 일지 (0)
        • Performance (0)
        • TroubleShooting (0)
        • Refactoring (0)
        • Code Style, Convetion (0)
        • Architecture (0)
      • Software Engineering (36)
        • Test (8)
        • 이론 (18)
        • Clean Code (10)
      • Java (72)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (13)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (16)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 35. 스레드 제어와 생명 주기(1)
상단으로

티스토리툴바