백엔드 면접 용어 사전
Java·객체지향carrier thread

캐리어 스레드

가상 스레드를 실제로 실행시키는 진짜 OS 스레드. ForkJoinPool로 관리되며 기본 개수는 CPU 코어 수다.

가상 스레드가 실제로 CPU 위에서 돌기 위해 올라타는 진짜 OS 스레드.

가상 스레드는 스스로 CPU를 쓸 수 없다. CPU를 점유할 수 있는 건 커널이 스케줄링하는 OS 스레드뿐이고, 가상 스레드는 그 위에 마운트되어야 비로소 실행된다. 이 받침대 역할을 하는 OS 스레드를 캐리어 스레드라고 부른다.

몇 개나 있나

  • 기본값 = 사용 가능한 CPU 코어 수
  • 조정 — = -Djdk.virtualThreadScheduler.parallelism=N
  • 관리 — = 전용 ForkJoinPool (Fork/Join 방식으로 작업을 나눠 갖는 스레드 풀)

8코어 머신이면 캐리어는 8개뿐이다. 그 위로 가상 스레드 10만 개가 번갈아 오르내린다.

마운트·언마운트의 흐름

가상 스레드 10,000개          캐리어 8개
     │                            │
  실행할 차례 ─── 마운트 ────────→ [코어에서 실행]
     │                            │
  블로킹 만남 ←── 언마운트 ───────┘  ← 스택을 힙에 보관, 캐리어 반납
     │
  대기 중(힙에 스택만 존재, OS 자원 0)
     │
  I/O 완료 ─── 재마운트 ────────→ [아무 캐리어 위에서 이어서 실행]

여기서 중요한 성질이 하나 있다 — 언마운트 후 재마운트될 때 같은 캐리어로 돌아간다는 보장이 없다. 그래서 "이 스레드에 값을 붙여 둔다"는 전제의 코드(ThreadLocal을 캐리어 기준으로 생각하는 코드)가 어긋난다.

캐리어가 막히면 — pinning

캐리어의 개수가 코어 수뿐이라는 게 곧 약점이다. 어떤 이유로 가상 스레드가 언마운트하지 못하고 캐리어를 붙잡으면, 그 캐리어는 블로킹이 끝날 때까지 다른 가상 스레드를 못 받는다. 8개 중 8개가 그렇게 되면 애플리케이션 전체가 멈춘다. 이 현상이 pinning이다.

면접 함정

  • "가상 스레드가 많으면 캐리어도 늘어난다" → 아니다. 캐리어는 코어 수로 고정이고, 그래서 pinning이 치명적이었다.
  • 관측 팁: 처리량이 캐리어 수 근처에서 정체되면 pinning이나 CPU 포화를 의심한다.

눈으로 확인하기

Thread.ofVirtual().start(() -> {
    System.out.println(Thread.currentThread());
    // VirtualThread[#21]/runnable@ForkJoinPool-1-worker-3
    //                                ^^^^^^^^^^^^^^^^^^^ 지금 올라탄 캐리어
});

앞부분이 가상 스레드, @ 뒤가 지금 그것을 실행 중인 캐리어다. 블로킹을 만났다가 재개하면 이 뒷부분이 바뀔 수 있다.

개수 조절

java -Djdk.virtualThreadScheduler.parallelism=16 -jar app.jar

기본값은 코어 수다. 늘린다고 처리량이 늘지는 않는다 — 캐리어가 부족해서 막히는 게 아니라면(즉 pinning이나 CPU 포화가 아니라면) 의미가 없다. 오히려 이 값을 만지고 싶어지는 상황이라면 병목이 다른 곳(커넥션 풀·다운스트림)에 있을 가능성이 높다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 모던 자바·스프링 — 가상 스레드·JEP 491·리액티브·jOOQ·Boot 4