본문 바로가기
인프런/김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성

[인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 3. 스레드 제어와 생명 주기 1

by hxxyeoniii 2025. 2. 2.

스레드 기본 정보

package thread.control;

import thread.start.HelloRunnable;

import static util.MyLogger.log;

public class ThreadInfoMain {
    public static void main(String[] args) {
        // main 스레드
        Thread mainThread = Thread.currentThread();
        log("mainThread = " + mainThread);
        log("mainThread.threadId()=" + mainThread.threadId());
        log("mainThread.getName()=" + mainThread.getName());
        log("mainThread.getPriority()=" + mainThread.getPriority());
        log("mainThread.getThreadGroup()=" + mainThread.getThreadGroup());
        log("mainThread.getState()=" + mainThread.getState());

        // myThread 스레드
        Thread myThread = new Thread(new HelloRunnable(), "myThread");
        log("myThread = " + myThread);
        log("myThread.threadId()=" + myThread.threadId());
        log("myThread.getName()=" + myThread.getName());
        log("myThread.getPriority()=" + myThread.getPriority());
        log("myThread.getThreadGroup()=" + myThread.getThreadGroup());
        log("myThread.getState()=" + myThread.getState());
    }
}

 

 

 

 

1. 스레드 생성

: Runnable 인터페이스 구현체와 스레드 이름 전달

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

 

2. 스레드 객체 정보

: Thread 클래스의 toString()은 스레드 ID, 스레드 이름, 우선순위, 스레드 그룹을 포함하는 문자열을 반환

log("myThread = " + myThread);

 

 

3. 스레드 ID

: 스레드 고유 식별자를 반환하는 메서드로 JVM 내의 각 스레드에 대해 유일

: 스레드가 생성될 때 할당되며 직접 지정 불가능

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

 

4. 스레드 이름

: 스레드 ID는 중복되지 않지만 이름은 중복 가능

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

 

5. 스레드 우선순위

: 우선순위는 1(가장 낮음) ~ 10(가장 높음)까지의 값으로 설정

: 기본값은 5

: setPriority()로 우선순위 변경 가능 but 실제 실행 순서는 JVM, OS에 따라 달라질 수 있음

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

 

6. 스레드 그룹

: 스레드 그룹은 스레드를 그룹화해 관리하는 기능 제공

: 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속함

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

 

7. 스레드 상태

: Thread.State 열거형에 정의된 상태 중 하나

   > NEW : 스레드가 아직 시작되지 않은 상태

   > RUNNABLE : 스레드가 실행 중이거나 실행될 준비가 된 상태

   > BLOCKED : 스레드가 동기화 락을 기다리는 상태

   > WAITING : 스레드가 다른 스레드의 특정 작업이 완료되기를 기다리는 상태

   > TIMED_WAITING : 일정 시간 동안 기다리는 상태

   > TERMINATED : 스레드가 실행을 마친 상태

 

* 실행 결과의 main 스레드는 실행 중이기 때문에 RUNNABLE, myThread는 생성 후 아직 시작하지 않았기에 NEW 상태


스레드의 생명 주기 - 설명

 

1. New(새로운 상태)

> 스레드가 생성되고 아직 시작되지 않은 상태

> Thread 객체가 생성되지만, start()가 호출되지 않은 상태

> ex) Thread thread = new Thread(runnable);

 

2. Runnable(실행 가능 상태)

> 스레드가 실행될 준비가 된 상태로 실제 CPU에서 실행될 수 있음

> start() 호출 시 스레드는 이 상태로 들어감

> Runnable 상태에 있는 모든 스레드가 동시에 실행되는 것은 아님. OS의 스케줄러가 각 스레드에 CPU 시간을 할당해 실행하기에 Runnable 상태에 있는 스레드는 스케줄러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행됨

> OS의 스케줄러 실행 대기열에 있든, CPU에서 실제 실행되고 있든 모두 RUNNABLE 상태로 자바에서는 둘을 구분할 수 없음

> 보통은 실행 상태라고 부름

 

3. Blocked(차단 상태)

> 스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태

> ex) synchronized(lock) {...} 코드 블록에 진입하려고 할 때, 다른 스레드가 이미 lock의 락을 가지고 있는 경우

 

4. Waiting(대기 상태)

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

> wait(), join() 메서드 호출 시 이 상태가 됨

> 스레드는 다른 스레드가 notify(), notifyAll()을 호출하거나 join()이 완료될 때까지 기다림

 

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

> 스레드가 특정 시간 동안 다른 스레드의 작업이 완료되기를 기다리는 상태

> sleep(long millis), wait(long timeout), join(long millis) 호출 시 이 상태가 됨

> 주어진 시간이 경과하거나 다른 스레드가 해당 스레드를 깨우면 이 상태에서 벗어남

 

6. Terminated(종료 상태)

> 스레드 실행이 완료된 상태

> 스레드가 정상적으로 종료되거나 예외가 발생해 종료된 경우 이 상태가 됨

> 한 번 종료되면 다시 시작할 수 없음

 

 

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

1. New -> Runnable : start() 호출

2. Runnable -> Blocked/Waiting/Timed Waiting : 락읕 얻지 못하거나, wait()나 sleep() 호출

3. Blocked/Waiting/Timed Waiting -> Runnable : 락을 얻거나 기다림이 완료된 경우

4. Runnable -> Terminated : run() 메서드 완료 시


스레드의 생명 주기 - 코드

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());
        log("myThread.start()");

        thread.start();

        Thread.sleep(1000);
        log("myThread.state3 = " + thread.getState());
        
        Thread.sleep(4000);
        log("myThread.state5 = " + thread.getState());

        log("end");
    }
    
    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            try {
                log("start");
                log("myThread.state2 = " + Thread.currentThread().getState());

                log("sleep() start");
                Thread.sleep(3000);
                log("sleep() end");

                log("myThread.state4 = " + Thread.currentThread().getState());
                log("end");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

 

 

 

 

실행 상태 확인

1. state1 = NEW

   > 메인 스레드를 통해 myThread 객체 생성

 

2. state2 = RUNNABLE

   > myThread.start() 호출로 실행 상태로 만듬

   > 실행 상태가 너무 빨리 지나가기에 메인 스레드에서는 myThread 상태 확인이 어려워 myThread에서 실행 중인 자신의 상태를 확인함

 

3. state3 = TIMED_WAITING

   > sleep() 호출로 3초간 대기하며 TIMED_WAITING 상태로 만듬

   > 이때 메인스레드가 1초간 대기 후 mainThread의 상태 확인

 

4. state4 = RUNNABLE

   > myThread는 3초 대기 후 다시 RUNNABLE 상태가 됨

 

5. state5 = TERMINATED

   > myThread는 run() 메서드 종료 후 TERMINATED 상태가 됨


체크 예외 재정의

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

public interface Runnable {
    void run();
}

 

 

 

자바에서 메서드를 재정의 할 때, 재정의 메서드가 지켜야할 예외야 관련된 규칙이 있다.

1. 체크 예외

   > 부모 메서드가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없다.

   > 자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.

 

2. 언체크(런타임) 예외

   > 예외 처리를 강제하지 않으므로 상관없이 던질 수 있다.

 

 

-> Runnable의 run은 아무런 체크 예외를 던지고 있지 않으므로 재정의 메서드도 체크 예외를 밖으로 던질 수 없음

 

 

 

안전한 예외 처리

체크 예외를 run()에서 던질 수 없게 강제함으로 개발자는 반드시 체크 예외를 try-catch 블록 내에서 처리하게 된다.

이는 예외 발생 시 예외가 적절히 처리되지 않아 프로그램이 비정상 종료되는 상황을 방지할 수 있다.

특히 멀티스레딩 환경에서 예외 처리를 강제함으로 스레드의 안정성과 일관성을 유지할 수 있다.


join 사용

JoinMainV1.java

1 ~ 50까지 합을 계산하는 스레드1과 51 ~ 100까지 합을 계산하는 스레드2로 1부터 100까지 합을 빠르게 구해보자.

package thread.control.join;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class JoinMainV1 {
    public static void main(String[] args) {
        log("Start");

        SumTask sumTask1 = new SumTask(1, 50);
        SumTask sumTask2 = new SumTask(51, 100);

        Thread thread1 = new Thread(sumTask1, "thread-1");
        Thread thread2 = new Thread(sumTask2, "thread-1");

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

        log("task1 result : " + sumTask1.result);
        log("task2 result : " + sumTask2.result);

        int sumAll = sumTask1.result + sumTask2.result;
        log("sumAll : " + 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);
        }
    }
}

 

실행 결과

-> main 스레드는 thread1, 2를 실행하고 바로 자신의 다음 코드를 실행함

-> 예상 결과와 다름.. thread1, 2가 종료된 후 main 스레드를 마지막에 종료하려면?

 

 

 

JoinMainV2.java

package thread.control.join;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class JoinMainV2 {
    public static void main(String[] args) throws InterruptedException {
        log("Start");

        SumTask sumTask1 = new SumTask(1, 50);
        SumTask sumTask2 = new SumTask(51, 100);

        Thread thread1 = new Thread(sumTask1, "thread-1");
        Thread thread2 = new Thread(sumTask2, "thread-1");

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

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

        log("task1 result : " + sumTask1.result);
        log("task2 result : " + sumTask2.result);

        int sumAll = sumTask1.result + sumTask2.result;
        log("sumAll : " + 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);
        }
    }

}

 

실행 결과

-> 정확히 5050이 계산됨

 

 

 

join 메서드 동작 확인

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

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

 

 

 

Waiting(대기 상태)

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

: join()을 호출하는 스레드는 대상 스레드가 TERMINATED가 될 때 까지 대기함, 호출 스레드는 WAITING 상태

: 대상 스레드가 TERMINATED 상태가 되면 호출 스레드는 다시 RUNNABLE 상태가 되며 다음 코드 수행

 

 

 

join의 두 가지 메서드

join() : 호출 스레드는 대상 스레드가 완료될 때까지 무한정 대기

join(ms) : 호출 스레드는 특정 시간 만큼만 대기 -> 대기하는 동안 TIMED_WAITING 상태가 됨