[Java] Runnable, Thread

나른한 개발자·2026년 1월 1일

f-lab

목록 보기
11/44

Runnable과 Thread는 자바에서 멀티 스레드를 위해 지원하는 기술이다. 자바 버전 별 지원 클래스는 다음과 같다.

  • Java5 이전: Runnable, Thread
  • Java5: Callle, Future, Executor, ExecutorService, Executors
  • Java7: Fork/Join, RecursiveTask
  • Java8: CompletableFuture
  • Java9: Flow

Thread 클래스

Thread 클래스는 스레드 생성을 위해 자바에서 미리 구현해둔 클래스이다. 다음과 같은 메서드를 제공한다.

  • sleep(): 현재 스레드 멈춘다. 자원을 넘겨주지는 않고 제어권을 넘겨주므로 데드락이 생길 수 있다.
  • interupt(): 다른 스레드를 깨워 interruptedException을 발생시킨다. 인터럽트가 발생한 스레드는 예외를 catch하여 다른 작업을 할 수 있다.
  • join(): 다른 스레드의 작업이 끝날때까지 기다리게한다. 스레드 순서를 제어할 때 사용할 수 있다.
@Test
void threadStart() {
    Thread thread = new MyThread();

    thread.start();
    System.out.println("Hello: " + Thread.currentThread().getName());
}

static class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread: " + Thread.currentThread().getName());
    }
}

// 출력 결과
// Hello: main
// Thread: Thread-2

Thread 클래스로 쓰레드를 구현하려면 이를 상속받는 클래스를 만들고, 내부에서 run 메소드를 구현해야 한다. 그리고 Thread의 start 메소드를 호출하면 run 메소드가 실행된다. 실행 결과를 보면 main 쓰레드가 아닌 별도의 쓰레드에서 실행됨을 확인할 수 있다.

이때 run()을 직접 호출하는 것이 아닌 start()를 실행시켜야한다는 것에 유념해야한다. run()을 직접 실행시키면 별도의 스레드가 아닌 메인 스레드에서 객체의 메서드를 호출하는 것이라, 메인 스레드가 해당 작업을 하게 된다.

start() 메서드를 호출하면 JVM과 운영체제가 협력하여 실행 흐름(스레드)가 만들어진다.

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        
        }
    }
}

위 코드는 start() 메서드이다. 이 코드에서는 3가지 작업을 수행한다.

  • 스레드가 실행 가능한지 검사
  • 스레드를 스레드 그룹에 추가
  • JVM이 스레드를 실행

스레드가 실행 가능한지 검사


스레드는 New, Runnable, Waiting, Timed Waiting, Terminated 총 5가지 상태가 있다. start() 메서드 가장 처음에는 해당 스레드가 실행 가능한 상태인지 (0인지) 확인한다. 만약 스레드 상태가 New(0)이 아니라면 IllegalThreadStateException 예외가 발생한다.

스레드를 스레드 그룹에 추가

그 다음 스레드를 스레드 그룹에 추가한다. 스레드 그룹이란 서로 관련있는 스레드를 하나의 그룹으로 묶어 다루기 위한 장치이다. 자바에서는 ThreadGroup 클래스를 제공한다. 스레드 그룹에 해당 스레드를 추가하면 스레드 그룹에 실행된 스레드가 있음을 알려주고, 관련작업들이 내부적으로 진행된다.

JVM이 스레드를 실행

그리고 start0 메서드를 호출하는데, 이것은 native 메서드로 선언되어있다. 이것은 JVM에 의해 호출되는데, 이것이 내부적으로 run을 호출하는 것이다. 그리고 스레드 상태도 runnable로 바뀌게 된다. 그래서 start는 여러번 호출할 수 없고 1번만 호출 가능하다.

@Test
void threadRun() {
    Thread thread = new MyThread();

    thread.run();
    thread.run();
    thread.run();
    System.out.println("Hello: " + Thread.currentThread().getName());
}

// 출력 결과
// Thread: main
// Thread: main
// Thread: main
// Hello: main

만약 다음과 같이 run을 직접 호출하면 새롭게 쓰레드가 만들어지지 않고, 메인 쓰레드에 의해 해당 메소드가 실행됨을 확인할 수 있다. 또한 여러 번 실행해도 아무런 문제가 없다. 그리고 출력 결과를 보면 main 메소드에 의해 실행됨을 실제로 확인할 수 있다.

Runnable 인터페이스

@FunctionalInterface
public interface Runnable {

    public abstract void run();
    
}

Runnbale 인터페이스는 1개의 메소드 만을 갖는 함수형 인터페이스이다. 그렇기 때문에 람다로도 사용 가능하다.

public class Thread implements Runnable {
    ...
}

이것은 쓰레드를 구현하기 위한 템플릿에 해당하는데, 해당 인터페이스의 구현체를 만들고 Thread 객체 생성 시에 넘겨주면 실행 가능하다. 앞서 살펴본 Thread 클래스는 반드시 run 메소드를 구현해야 했는데, Thread 클래스가 Runnable를 구현하고 있기 때문이다.

기존에 Thread로 작성되었던 코드를 Runnable로 변경하면 다음과 같다. 마찬가지로 별도의 쓰레드에서 실행됨을 확인할 수 있다.

@Test
void runnable() {
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("Thread: " + Thread.currentThread().getName());
        }
    };

    Thread thread = new Thread(runnable);
    thread.start();
    System.out.println("Hello: " + Thread.currentThread().getName());
}

// 출력 결과
// Hello: main
// Thread: Thread-1

Thread와 Runnable 비교

  • Runnable은 익명 객체 및 람다로 사용할 수 있지만 Thread는 별도의 클래스를 만들어야해서 번거롭다.
  • Java에서는 다중 상속이 불가능하므로 Thread 클래스를 상속받으면 다른 클래스를 상속받을수 없어 좋지 않다.
  • Thread 클래스는 매우 커서 이름, 스택 사이즈, 우선순위 등 많은 상태값과 메서드를 가지고 있다. 따라서 Thread 클래스를 상속받으면 Thread 클래스에 구현된 코드들에 의해 더 많은 자원(메모리와 시간 등)을 필요로 하므로 자원 사용량이 적은 Runnable이 주로 사용된다.
  • Thread 관련 기능의 확장이 필요한 경우에는 Thread 클래스를 상속받아 구현해야 할 때도 있다. 하지만 거의 대부분의 경우에는  Runnable 인터페이스를 사용하면 해결 가능하다.

Thread와 Runnable의 단점 및 한계

  • 지나치게 저수준의 API(쓰레드의 생성)에 의존함: 스레드를 어떻게 만드는지는 애플리케이션을 개발하는 개발자의 관심사와는 거리가 멀다. 스레드 생성/시작/종료 등 세세한 작업을 모두 관리해야해 코드가 복잡해진다.
  • 값의 반환이 불가능: Runnable의 run()은 반환 타입이 void이다. 즉 어떤 스레드에 작업을 시켜도 실행 결과 값을 알수 없다. 결국 공유 객체를 만들어서 값을 담아두거나, 멤버 변수에 저장하는 등 번거로운 우회 방법을 써야 한다.
  • 매번 쓰레드 생성과 종료하는 오버헤드가 발생
  • 쓰레드들의 관리가 어려움: 스레드가 무수히 늘어나면 컴퓨터 메모리가 바닥나서 시스템이 멈출 수 있다. 어떤 스레드가 실행중인지 등을 알기 힘들다.

이러한 문제 때문에 Java5 버전부터는 쓰레드를 사용하는 다양한 방법(Executor, ExecutorService 등)을 꾸준히 발전시켜오고 있다.

출처: https://mangkyu.tistory.com/258 [MangKyu's Diary:티스토리]

Runnable과 Thread는 자바5 이전 버전에서 멀티 스레딩을 위해 지원하는 기술이다. Thread클래스는 이를 상속받아서 run() 메서드를 오버라이딩하는 방식으로 스레드를 생성한다. 구현이 직관적이지만 단일 상속만 지원하므로 Thread를 상속받으면 다른 클래스를 상속받을 수 없고 많은 변수와 메서드를 가지고 있어 클래스가 무겁다는 단점이 있다.
Runnable은 함수형 인터페이스로 run() 메서드 하나만 가지고 있다. 이를 구현한 클래스의 인스턴스를 Thread 생성자에 전달하는 방식으로 사용한다. 가장 큰 장점은 다른 클래스를 상속받을 수 있어 설계의 유연성이 높고, 인터페이스이기 때문에 여러 스레드가 같은 객체를 공유하기 쉽습니다.(Runnable을 구현한 객체 하나를 여러 Thread의 생성자에 넘겨줄 수 있다는 뜻) 또한 람다식으로도 구현 가능해서 코드가 간결해집니다.
하지만 이 Thread와 Runnable은 저수준 API에 의존하기 때문에 개발자가 스레드를 생성/종료하는 것을 관리해야한다는 복잡성이 있다. 또한 run() 메서드가 리턴값이 없기 때문에 작업 실행 후 결과값을 받으려면 공유자원을 사용하여 우회해야한다는 한계가 존재한다.

profile
Start fast to fail fast

0개의 댓글