자바의 스레드 (Thread)

한눈에 보기

스레드는 프로세스 안의 실행 흐름 단위로 힙을 공유하고 스택을 독립적으로 갖는다. 생성 방법과 생명주기 여섯 상태, 동기화 문제, Java 21 가상 스레드까지 정리한다.

최종 업데이트 2026.09.17

스레드(Thread)는 프로세스 안에서 실행되는 흐름의 단위로, 자바에서는 java.lang.Thread 클래스가 이를 표현하며 모든 자바 프로그램은 main 스레드에서 시작한다. 같은 프로세스의 스레드들은 힙(heap)과 메서드 영역을 공유하고 각자 독립적인 스택(stack)과 프로그램 카운터를 가지므로, 공유 데이터에 동시 접근할 때는 동기화가 필요하다. Java 21부터는 운영체제 스레드에 일대일로 묶이는 플랫폼 스레드(platform thread)에 더해 JVM이 관리하는 경량 가상 스레드(virtual thread)가 정식으로 추가되었다.

스레드 생성 방법

스레드를 만드는 고전적인 방법은 Thread를 상속해 run()을 오버라이딩하거나, Runnable을 구현해 Thread 생성자에 넘기는 것이다. 상속 방식은 다른 클래스를 상속할 수 없게 되고 작업과 실행 주체가 결합되므로, 작업만 분리해 전달하는 Runnable 방식이 권장된다. 반환값이나 예외가 필요하면 Callable<V>를 쓴다.

// Runnable, 람다로 간결하게
Thread worker = new Thread(() -> System.out.println("작업 실행"));
worker.start(); // run()을 직접 부르면 새 스레드가 생기지 않는다

// 실무 표준: 스레드 풀에 작업 제출
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> 1 + 1); // Callable
Integer result = future.get(); // 완료까지 대기
pool.shutdown();

// Java 21: 작업마다 가상 스레드
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> callExternalApi());
}

start()와 run()의 차이는 단골 질문이다. start()는 JVM에 새 스레드 생성을 요청하고 그 스레드에서 run()이 실행되게 하지만, run()을 직접 호출하면 현재 스레드에서 평범한 메서드로 실행될 뿐이다. 또한 실무에서는 스레드를 직접 생성하기보다 ExecutorService 같은 스레드 풀로 생성 비용을 상쇄하고 개수를 통제하는 것이 표준이다.

스레드의 생명주기

자바 스레드는 여섯 가지 상태를 오간다. 생성 직후의 NEW, 실행 중이거나 스케줄링을 기다리는 RUNNABLE, 고유 락 획득을 기다리는 BLOCKED, wait()나 join()으로 무기한 기다리는 WAITING, sleep(ms)처럼 시간 제한을 두고 기다리는 TIMED_WAITING, 실행이 끝난 TERMINATED다. 상태는 getState()로 확인할 수 있으며, 스레드 덤프를 분석해 교착 상태(deadlock)나 병목을 진단할 때 이 상태 정보가 핵심 단서가 된다.

메모리 구조와 동기화 문제

JVM은 스레드마다 별도의 스택을 할당하므로 지역 변수는 본질적으로 스레드 안전하다. 문제는 모든 스레드가 공유하는 힙의 객체다. 두 스레드가 같은 필드에 동시에 count++ 같은 복합 연산을 수행하면 읽기, 증가, 쓰기 사이에 서로의 갱신을 덮어쓰는 경쟁 상태(race condition)가 생긴다. 또한 CPU 캐시와 명령 재배치 때문에 한 스레드의 쓰기가 다른 스레드에 보이지 않는 가시성(visibility) 문제도 있다.

해결 수단으로는 원자성과 가시성을 함께 주는 synchronized와 ReentrantLock, 가시성만 보장하는 volatile, CAS(Compare-And-Swap) 기반의 AtomicInteger 같은 원자 클래스, 그리고 ConcurrentHashMap 같은 동시성 컬렉션이 있다. 가장 좋은 전략은 애초에 공유 가변 상태를 줄이는 것으로, 불변 객체와 스레드 한정(thread confinement)이 그 방법이다.

플랫폼 스레드와 가상 스레드

구분플랫폼 스레드가상 스레드
매핑OS 스레드와 1대1소수의 캐리어 스레드에 다대다
생성 비용큼, 스택 약 1MB 예약매우 작음, 힙에 작은 스택
적정 개수수백에서 수천수백만도 가능
블로킹 시OS 스레드가 통째로 대기캐리어에서 내려와 양보
적합한 작업CPU 집약 작업IO 대기가 많은 작업

가상 스레드는 블로킹 IO를 만나면 스택을 힙에 저장하고 캐리어 스레드를 반납하므로, 스레드당 요청 모델의 단순한 코드로 높은 동시성을 얻는다. 다만 CPU 집약 작업에는 이점이 없고, 풀에 넣어 재사용하는 대신 작업마다 새로 만드는 것이 올바른 사용법이다. 이 밖에 JVM 종료를 막지 않는 데몬 스레드(daemon thread), 다른 스레드의 종료를 기다리는 join(), 협조적 중단 신호인 인터럽트(interrupt) 메커니즘도 기본 소양으로 알아 두어야 한다.

면접에서 이렇게 물어본다

  • 프로세스와 스레드의 차이를 메모리 공유 관점에서 설명해 보라.
  • Thread의 start()와 run()의 차이는 무엇인가.
  • 자바 스레드의 상태 여섯 가지와 각 상태로 전이되는 조건을 설명해 보라.
  • 경쟁 상태와 가시성 문제가 무엇이며 각각 어떤 도구로 해결하는가.
  • 가상 스레드가 기존 스레드 풀 모델의 어떤 한계를 해결하는지 설명해 보라.
Java 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기