김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 : 스레드 제어와 생명 주기1

jkky98·2024년 8월 1일
0

Java

목록 보기
36/51

Thread 기본 정보

  1. log(myThread) or System.out.println(myThread) - myThread 객체의 ToString() 결과를 준다. 스레드 ID, 스레드 이름, 우선순위 그룹 정보를 담은 String을 반환한다.
- Thread[스레드이름, 우선순위, 스레드그룹이름]
- Thread[worker-1,5,main]
  1. myThread.threadId() - 스레드의 고유 식별자를 반환하는 메서드, 이 ID는 JVM 내에서 유일. 생성될 때 할당되며 직접 지정 불가능

  2. myThread.getName() - 스레드 이름 반환, ID와 달리 중복 가능

  3. myThread.getPriority() - 1~10(가장 높음)까지 값으로 설정 가능하며 디폴트는 5다. 우선순위는 스케줄러가 어떤 스레드를 우선 실행할 지 결정하는 데 사용. 절대적인 명령은 아니며 JVM 구현과 운영체제가 참고해서 쓴다고만 생각하자.

  4. myThread.getThreadGroup() - 스레드가 속한 스레드 그룹 반환, 스레드 그룹이란 스레드를 그룹화하여 관리할 수 있는 기능이며 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속한다. 이때 부모 스레드란 우리가 이때동안 커스텀 스레드를 생성했을 때 이를 생성시킨 스레드는 main스레드이다. 즉 main 스레드는 부모 스레드가 된다. 즉 생성된 스레드는 생성한 스레드를 부모로 간주한다.

  5. myThread.getState()는 스레드의 현재 상태를 반환하는 메서드이다.

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

Thread Life cycle(스레드 생명 주기)

스레드의 생명주기를 스레드의 state로 설명하자면 다음과 같다.

  • NEW -> (Runnable, Blocked, Waiting, Timed Waiting) -> Terminated

State: Runnable(실행 상태)

스레드가 실행될 준비가 된 상태이며 이 상태에서 스레드는 실제로 CPU에서 실행될 수있다.

start()메서드 호출시 이 상태로 들어간다. Runnable인데 실행 상태라 읽는 이유는 자바에서는 스케줄러의 실행 대기열에 있든 CPU에서 실제로 실행되고 있든 모두 Runnable 상태로 인식하기 때문이다.(둘을 구분할 수 없다.)

State: Blocked(차단 상태)

스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태이다. 아직 배우지 않아 pass

State: Waiting(대기 상태)

스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다. wait(), join() 메서드 호출시 이 상태가 된다. 이것도 뒤에서

State: Timed Waiting (시간 제한 대기 상태)

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

State: Terminated (종료 상태)

스레드의 실행이 완료도니 상태, 정상적으로 종료 혹은 예외 발생하여 종료된 경우도 이 상태로 마무리 된다.

코드로 생명주기 확인해보기


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()); // TimedWating
        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());
            log("sleep() start");
            Thread.sleep(3000);
            log("sleep() end");
            log("myThread.state4 = " + Thread.currentThread().getState());
            log("end");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
// 결과
16:43:58.010 [     main] myThread.state1 = NEW
16:43:58.012 [     main] myThread.start()
16:43:58.013 [ myThread] start
16:43:58.013 [ myThread] myThread.state2 = RUNNABLE
16:43:58.013 [ myThread] sleep() start
16:43:59.015 [     main] myThread.state3 = TIMED_WAITING
16:44:01.018 [ myThread] sleep() end
16:44:01.019 [ myThread] myThread.state4 = RUNNABLE
16:44:01.020 [ myThread] end
16:44:03.021 [     main] myThread.state5 = TERMINATED
16:44:03.022 [     main] end

myThread를 추적하기 위해 mainThread를 잘 이용해야 한다. myThread가 RUNNABLE 상태이면 자신의 상태를 출력할 수 있지만 TIMED_WATING 상태일 때는 자신의 상태를 출력할 수 없으므로 main이 출력해주어야 한다.(당연하다 자는 사람에게 자고있다는 대답을 들을 수 없지 않으니까)

체크 예외 재정의

Runnable 구현시 run 오버라이드 메서드에서 가끔 예외를 던질수도 없고 필수적으로 잡아야할 경우가 있었다. 예로 Thread.sleep()을 실행할 때 그러했다.

다시 예외 개념을 정리해보면 체크예외는 명시해서 던지거나, 잡아야 했으며 언체크예외는 아무것도 해주지 않아도 알아서 던져졌다.

공부한 체크예외의 조건에 하나 더 알아두어야 할 점이 존재한다.

부모 메서드가 체크 예외를 던지지 않는 경우 오버라이딩된 자식 메서드도 체크 예외를 던질 수 없다. 또한 자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.

Runnable 인터페이스의 run()은 아무런 체크 예외를 던지지 않는다. 그러므로 오버라이딩 run()은 어떠한 체크예외도 던질 수 없다. 그러므로 오버라이딩 run()에서 체크 예외를 던져대는 어떤 무언가를 호출한다면 오버라이딩 run()에서 무조건 잡아야 된다.

안전한 예외 처리

이렇게 Runnable 인터페이스의 run에서 예외를 던지지 않는 이유는 의도적이다. 개발자가 반드시 이를 run()에서 처리하도록 자바가 유도한 것이다. 이는 예외 발생 시 예외가 적절히 처리되지 않아 프로그램이 비정상 종료되는 상황을 방지해준다

하지만 이러한 컨셉은 자바 초창기 기조이며 최근에는 언체크를 더 선호하는 것은 분명하다. 그런 관점에서 run()이 아닌 다른 무언가가 쓰일 수 있겠구나 일단 생각하고 넘어가자.(그리고 run()에서 예외를 잡는 경우가 그렇게 잦지않다.)

Join

개념은 간단하다. 하나의 스레드가 실행상태 끝나는 것을 다른 한 스레드가 기다리도록 하는 것이 Join 기능이다. 기다리는 스레드는 Waiting(대기 상태)로 무기한 기다린다.

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, "thread1");
        Thread thread2 = new Thread(task2, "thread2");

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

        // 스레드1,2 종료될 때 까지 main 스레드 대기
        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);
        }
    }
}

위의 코드에 thread1.join, thread2.join을 실행시키는 스레드는 main스레드이다. main스레드가 thread1.join을 실행시켰다면 main스레드는 thread1.run()작업이 끝날 때까지 Waiting으로 기다린다.

그리고 thread2.join을 다시 만나 thread2를 기다릴 것이다. 하지만 thread2는 thread1과 거의 동시에 실행되었을 테니 거의 끝나있거나 이미 끝나있을 것이다. 그러므로 main은 곧바로 빠져나와 그다음 main 스레드의 작업인 sumAll을 진행하여 로직을 마무리한다.

join()은 두 가지 메서드가 존재한다. 이전에 쓴 join()과 다르게 join(ms)를 지원한다. 이는 Wating 상태가 무기한으로 스레드를 기다릴 수 있는 확률이 존재하므로 지정한 시간(ms)가 지나면 다시 Runnable상태가 되면서 다음 코드를 수행하게끔 한다.

profile
자바집사의 거북이 수련법

0개의 댓글