개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Java › 스레드와 동시성

스레드의 생성과 상태

run()을 직접 부르면 왜 새 스레드가 생기지 않나

Thread 객체를 만드는 것과 스레드가 실제로 실행되는 것은 다른 일입니다. 스레드 하나가 만들어져서 끝날 때까지 어떤 상태를 거치는지를 t.getState()로 따라가 봅니다.

실행으로 대조 · JDK 21에서 스물네 가지 조합을 실행해 대조

이 실습에 나오는 개념

프로세스와 스레드
프로세스는 실행 중인 프로그램으로, 운영체제로부터 독립된 메모리 공간을 할당받는다. 스레드는 프로세스 안에서 실제로 작업을 수행하는 실행 흐름으로, 같은 프로세스의 스레드들은 메모리(힙, 코드, 데이터)를 공유하고 스택만 따로 가진다. 그래서 스레드 간에는 데이터를 주고받기 쉽지만 동시에 같은 데이터를 건드리는 문제가 생긴다.
스레드의 상태
자바의 스레드는 만들어져서 끝날 때까지 여섯 가지 상태 가운데 하나에 있으며, t.getState()로 확인할 수 있다. NEW는 Thread 객체만 만들고 아직 시작하지 않은 상태, RUNNABLE은 실행 중이거나 실행할 차례를 기다리는 상태, TERMINATED는 실행이 끝난 상태다. 그 사이에 다른 스레드가 쥔 락을 기다리면 BLOCKED, 기한 없이 기다리면 WAITING, 시간을 정해 두고 기다리면 TIMED_WAITING이 된다. 한 번 TERMINATED가 된 스레드는 다른 상태로 돌아가지 않는다.
start()와 run()
start()는 새 스레드를 만들어 그 스레드가 run()을 실행하게 하는 메서드이고, run()은 스레드가 할 일을 담은 보통의 메서드다. 그래서 run()을 직접 부르면 새 스레드가 생기지 않고, 부른 스레드가 그 코드를 순서대로 실행한다. start()는 Thread 객체 하나에 한 번만 부를 수 있으며, 두 번째로 부르면 IllegalThreadStateException이 발생한다. 같은 작업을 다시 실행하려면 Thread 객체를 새로 만든다.
join()
다른 스레드가 끝날 때까지 지금 스레드를 기다리게 하는 메서드다. t.join()을 부른 스레드는 t가 끝날 때까지 다음 줄로 가지 않으므로, 다른 스레드가 만든 결과를 받아서 써야 할 때 쓴다. 기다리는 동안 부른 쪽 스레드의 상태는 WAITING이고, join(1000)처럼 시간을 주면 TIMED_WAITING이다. 이미 끝났거나 아직 시작하지 않은 스레드에 부르면 기다리지 않고 바로 돌아온다.
인터럽트(interrupt)
기다리고 있는 스레드에게 그만 기다리라고 보내는 신호다. t.interrupt()를 부르면 t에 "interrupt를 받았다"는 표시가 켜지고, t가 sleep이나 join, wait로 기다리는 중이었다면 그 메서드가 InterruptedException을 던지며 바로 깨어난다. 스레드를 강제로 멈추는 기능은 아니어서, 신호를 받은 뒤 어떻게 할지는 그 스레드의 코드가 정한다. synchronized의 락을 기다리는 BLOCKED 상태의 스레드는 interrupt로 깨어나지 않는다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. new Thread(task, "worker"): Thread 객체 t를 만듭니다. 아직 객체만 있을 뿐이고, 새 실행 흐름(스레드)은 시작되지 않았습니다. 그래서 t.getState()는 NEW입니다.
  2. t.run(): run()은 보통의 메서드 호출입니다. 새 스레드는 만들어지지 않고, main 스레드가 task의 코드를 직접 실행합니다. t의 상태는 NEW 그대로입니다.
  3. Thread.currentThread().getName(): task의 첫 줄을 실행하는 스레드의 이름은 main입니다. worker라는 이름의 스레드는 생긴 적이 없습니다.
  4. Thread.sleep(1000): main 스레드가 직접 1초 동안 쉽니다. 그동안 main은 t.run() 안에 머물러 다음 줄로 가지 못합니다. 1초 뒤에 task가 끝나고 t.run()이 돌아옵니다.
  5. Thread.sleep(100) 뒤 t.getState(): t는 start()가 불린 적이 없으므로 여전히 NEW입니다. 작업은 실행됐지만 t라는 스레드가 실행한 것이 아닙니다.
  6. t.join(): 시작된 적이 없는 스레드는 기다릴 것이 없으므로 join()은 바로 돌아옵니다.
  7. t.getState(): 마지막까지 NEW입니다. task는 실행됐지만 t는 한 번도 시작되지 않았습니다.

실행 결과

코드

Runnable task = () -> {
    System.out.println(Thread.currentThread().getName());   // 이 줄을 실행하는 스레드의 이름
    try {
        Thread.sleep(1000);                 // 1초 동안 쉰다
    } catch (InterruptedException e) {
        System.out.println("InterruptedException");
    }
};

Thread t = new Thread(task, "worker");
System.out.println(t.getState());

t.run();                                    // 메서드를 직접 호출
Thread.sleep(100);                          // 작업이 진행되도록 잠깐 기다린다
System.out.println(t.getState());
t.join();                                   // t가 끝날 때까지 기다린다
System.out.println(t.getState());

이 모형의 전제

스물네 가지 조합의 결과(작업을 실행한 스레드 이름, 중간과 마지막의 t.getState(), 발생한 예외, 작업이 끝날 때의 interrupt 표시)를 이 PC의 JDK 21에서 실행한 결과와 대조했습니다. 화면의 코드는 main이 Thread.sleep(100)으로 잠깐 기다린 뒤 상태를 보지만, 이 100밀리초는 "대개 충분한" 시간일 뿐 보장된 것이 아닙니다. 대조할 때는 시간에 기대지 않도록 t의 상태가 RUNNABLE을 벗어나 그대로 머무를 때까지 되풀이해 확인했습니다. start() 바로 뒤에 t.getState()를 부르면 RUNNABLE일 수도, 이미 다른 상태일 수도 있어서 결과로 삼지 않았습니다. RUNNABLE은 "실행 중"과 "CPU 차례를 기다리는 중"을 구분하지 않으며, 자바는 이 둘을 따로 알려 주지 않습니다. WAITING은 join 말고도 Object.wait()나 LockSupport.park()로도 생기지만 여기서는 join만 다뤘습니다. 여러 스레드가 함께 쓰는 데이터의 문제는 "경쟁 상태" 실습에서, 스레드를 미리 만들어 두고 다시 쓰는 방법은 "스레드 풀" 실습에서 다룹니다.

앞 실습: 프로세스와 스레드 · 다음 실습: 경쟁 상태

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요