CPU와 Java 쓰레드 — 컨텍스트 스위칭까지

방지환·2026년 7월 1일

Java

목록 보기
20/20

CPU와 Java 쓰레드 — 컨텍스트 스위칭까지

CPU가 어떻게 명령을 실행하고, 여러 쓰레드를 어떻게 번갈아 처리하는지,
그리고 Java 쓰레드가 그 위에서 어떻게 동작하는지를 흐름 중심으로 정리한 문서.


목차

  1. CPU 기본 구조
  2. 명령어 실행 흐름 (Instruction Cycle)
  3. 프로세스와 쓰레드
  4. 컨텍스트 스위칭 (Context Switching)
  5. CPU 스케줄링
  6. Java 쓰레드의 동작
  7. Java 쓰레드 상태와 전이
  8. 멀티코어 · 병렬성 · 동시성
  9. 성능 관점 정리
  10. 요약

1. CPU 기본 구조

CPU(Central Processing Unit)는 명령어를 해석하고 실행하는 부품이다.
핵심 구성요소는 다음과 같다.

구성요소역할
ALU (산술논리장치)덧셈, 비교, 논리연산 등 실제 계산 수행
CU (제어장치)명령어를 해석하고 각 부품에 신호를 보냄
레지스터 (Register)CPU 내부의 초고속 저장 공간 (연산 중간값, 주소 등)
캐시 (L1/L2/L3)메모리보다 빠른 임시 저장소, 자주 쓰는 데이터 보관

대표 레지스터

  • PC (Program Counter): 다음에 실행할 명령어의 주소
  • IR (Instruction Register): 현재 실행 중인 명령어
  • SP (Stack Pointer): 스택의 최상단 주소
  • 범용 레지스터: 연산에 쓰이는 값 저장

속도 계층 (빠름 → 느림)

레지스터  >  L1 캐시  >  L2 캐시  >  L3 캐시  >  메인 메모리(RAM)  >  디스크(SSD/HDD)
 (1 cycle)   (~4)       (~10)      (~40)        (~100+ cycle)         (수만 cycle)

CPU는 매우 빠르지만 메모리는 상대적으로 느리다. 그래서 캐시가 존재한다.


2. 명령어 실행 흐름 (Instruction Cycle)

CPU는 아래 사이클을 끊임없이 반복한다. 이것이 CPU가 "동작한다"의 본질이다.

 ┌─────────────────────────────────────────────────┐
 │                                                   │
 │   ① Fetch  ──▶  ② Decode  ──▶  ③ Execute        │
 │   (인출)         (해석)          (실행)            │
 │      ▲                              │             │
 │      │                              ▼             │
 │      └──────  ④ Write-back  ◀───────┘             │
 │              (결과 저장)                           │
 └─────────────────────────────────────────────────┘
              PC 증가 → 다음 명령어 반복
  1. Fetch (인출): PC가 가리키는 주소에서 명령어를 메모리 → IR로 가져온다.
  2. Decode (해석): CU가 명령어의 종류(더하기? 이동? 점프?)를 판별한다.
  3. Execute (실행): ALU가 연산을 수행하거나 메모리에 접근한다.
  4. Write-back (저장): 결과를 레지스터나 메모리에 기록한다.
  5. PC를 다음 명령어로 이동시키고 ①로 돌아간다.

파이프라이닝 (Pipelining)

현대 CPU는 위 단계를 겹쳐서 처리해 처리량을 높인다.

시간 →
명령1:  Fetch  Decode Execute Write
명령2:         Fetch  Decode  Execute Write
명령3:                Fetch   Decode  Execute Write

한 명령이 끝나길 기다리지 않고, 앞 명령이 Decode 중일 때 다음 명령을 Fetch한다.


3. 프로세스와 쓰레드

프로세스 (Process)

  • 실행 중인 프로그램의 인스턴스.
  • 독립된 메모리 공간(Code, Data, Heap, Stack)을 가진다.
  • 프로세스 간에는 메모리를 공유하지 않는다 (격리).

쓰레드 (Thread)

  • 프로세스 안에서 실제로 CPU가 실행하는 실행 흐름의 단위.
  • 같은 프로세스의 쓰레드들은 Code / Data / Heap을 공유한다.
  • 단, Stack과 레지스터(PC 포함)는 쓰레드마다 별도로 가진다.
┌──────────────── Process ────────────────┐
│  Code   Data   Heap   (공유)             │
│                                          │
│  ┌ Thread 1 ┐  ┌ Thread 2 ┐  ┌ Thread 3┐│
│  │  Stack   │  │  Stack   │  │  Stack  ││
│  │  PC/Reg  │  │  PC/Reg  │  │  PC/Reg ││  ← 쓰레드별 고유
│  └──────────┘  └──────────┘  └─────────┘│
└──────────────────────────────────────────┘

쓰레드는 Heap을 공유하기 때문에 데이터 공유가 쉽지만,
동시에 접근하면 동시성 문제(race condition) 가 발생한다 → 그래서 동기화가 필요.


4. 컨텍스트 스위칭 (Context Switching)

왜 필요한가?

CPU 코어 1개는 한 순간에 딱 하나의 쓰레드만 실행할 수 있다.
그런데 실행할 쓰레드는 여러 개다. 그래서 CPU는 아주 짧은 시간(예: 수 ms)마다
쓰레드를 번갈아 가며 실행한다 → 사람 눈엔 동시에 도는 것처럼 보인다.

이때 실행 중이던 쓰레드의 상태를 저장하고, 다음 쓰레드의 상태를 복원하는 작업이
바로 컨텍스트 스위칭이다.

컨텍스트란?

쓰레드가 실행을 재개하는 데 필요한 모든 정보:

  • PC (어디까지 실행했는지)
  • 레지스터 값들
  • 스택 포인터
  • (프로세스 전환이면) 메모리 맵 정보 등

이 정보는 커널의 PCB(Process Control Block) / TCB(Thread Control Block) 에 저장된다.

흐름

  Thread A 실행 중
        │
        ▼
 ┌──────────────────┐
 │ 인터럽트/타임슬라이스 만료 │   ← 트리거 발생
 │ 또는 I/O 대기, yield 등  │
 └──────────────────┘
        │
        ▼
 ① Thread A의 컨텍스트 저장  (레지스터/PC → TCB_A)
        │
        ▼
 ② 스케줄러가 다음 쓰레드 B 선택
        │
        ▼
 ③ Thread B의 컨텍스트 복원  (TCB_B → 레지스터/PC)
        │
        ▼
  Thread B 실행 재개

컨텍스트 스위칭이 일어나는 순간

  • 타임 슬라이스(time quantum) 만료: 할당된 시간을 다 씀
  • I/O 요청: 디스크/네트워크 대기 → CPU를 놀릴 수 없으니 다른 쓰레드에 넘김
  • 더 높은 우선순위 쓰레드 등장 (선점, preemption)
  • 락 대기 / sleep / wait 등 블로킹
  • 인터럽트 발생 (하드웨어 신호)

비용 (Overhead) ⚠️

컨텍스트 스위칭은 공짜가 아니다. 그 자체는 "일"을 하지 않는 순수 오버헤드.

  • 레지스터 저장/복원 비용
  • 캐시 오염(Cache pollution): 새 쓰레드가 오면 기존 쓰레드가 캐시에 올려둔 데이터가 밀려남 → 캐시 미스 급증
  • TLB(주소 변환 캐시) 플러시 (특히 프로세스 전환 시)

쓰레드가 너무 많으면 스위칭 비용이 실제 작업보다 커지는
쓰레드 스래싱(thrashing) 이 발생할 수 있다.
"쓰레드 많다고 무조건 빠른 게 아니다."


5. CPU 스케줄링

스케줄러(OS 커널)가 "다음에 어떤 쓰레드를 실행할지" 결정한다.

대표 알고리즘

알고리즘설명
FCFS먼저 온 순서대로 (First Come First Served)
SJF짧은 작업 먼저 (Shortest Job First)
Round Robin각 쓰레드에 동일한 타임 슬라이스를 순환 배분 (선점형)
Priority우선순위 높은 쓰레드 먼저
MLFQ다단계 피드백 큐, 실사용 OS가 많이 채택

선점 vs 비선점

  • 선점형(Preemptive): OS가 강제로 CPU를 뺏을 수 있음 (대부분의 현대 OS)
  • 비선점형(Non-preemptive): 쓰레드가 스스로 놓아줄 때까지 기다림

6. Java 쓰레드의 동작

JVM과 OS 쓰레드의 관계

Java의 Thread는 대부분의 JVM(HotSpot)에서 1:1로 OS 네이티브 쓰레드에 매핑된다.
즉, new Thread().start()를 하면 실제로 OS 커널 쓰레드가 하나 생긴다.
→ 스케줄링과 컨텍스트 스위칭은 결국 OS가 담당한다.

Java Thread  ──1:1──▶  OS Native Thread  ──▶  CPU Core에서 실행

기본 사용

// 1) Runnable 구현
Runnable task = () -> System.out.println("실행: " + Thread.currentThread().getName());
Thread t = new Thread(task);
t.start();   // start()가 OS 쓰레드 생성 → run() 실행
             // ※ run()을 직접 호출하면 그냥 현재 쓰레드에서 실행됨 (새 쓰레드 X)

// 2) ExecutorService (실무 권장) — 쓰레드 풀 재사용으로 생성 비용 절감
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(task);
pool.shutdown();

쓰레드 풀을 쓰는 이유: 쓰레드 생성/소멸은 비싸다.
미리 만들어 두고 재사용하면 생성 비용 + 컨텍스트 스위칭 관리가 유리하다.

동시성 문제와 동기화

같은 Heap(공유 자원)을 여러 쓰레드가 건드리면 문제가 생긴다.

class Counter {
    private int count = 0;
    // synchronized: 한 번에 한 쓰레드만 진입 (락 획득)
    public synchronized void increment() { count++; }
}
  • synchronized, ReentrantLock: 상호배제(mutual exclusion)
  • volatile: 가시성 보장 (캐시가 아닌 메인 메모리에서 읽도록)
  • java.util.concurrentAtomicInteger, ConcurrentHashMap 등 활용

⚠️ 락 대기는 곧 블로킹 → 컨텍스트 스위칭을 유발한다. 락 경합이 심하면 성능 저하.

가상 쓰레드 (Virtual Threads, Java 21+)

  • 기존 쓰레드(플랫폼 쓰레드)는 OS 쓰레드와 1:1이라 수만 개 만들면 무겁다.
  • 가상 쓰레드는 JVM이 관리하는 경량 쓰레드로, 소수의 OS 쓰레드(캐리어) 위에서 다중화된다.
  • I/O 대기 시 OS 쓰레드를 붙잡지 않고 양보 → 컨텍스트 스위칭 비용이 훨씬 저렴.
  • 수백만 개의 동시 작업(특히 I/O 중심)에 유리.
// Java 21+
Thread.startVirtualThread(() -> {
    // 블로킹 I/O를 해도 OS 쓰레드를 점유하지 않음
});

7. Java 쓰레드 상태와 전이

Thread.State enum 기준 6가지 상태:

        start()
  NEW ──────────▶ RUNNABLE ◀───────────────┐
                    │  ▲                     │
     synchronized   │  │ 락 획득             │ notify()/
     블록 진입 대기  │  │                     │ 시간 만료
                    ▼  │                     │
                 BLOCKED                     │
                    │                        │
     wait()/join() │                   WAITING /
     sleep()       └──────────────▶  TIMED_WAITING
                                          │
                          run() 종료      │
                    RUNNABLE ─────────▶ TERMINATED
상태의미
NEW생성됨, 아직 start() 안 함
RUNNABLE실행 중이거나 실행 가능 (CPU 배정 대기 포함)
BLOCKED모니터 락 획득을 기다림 (synchronized)
WAITING다른 쓰레드의 신호를 무한 대기 (wait(), join())
TIMED_WAITING시간 제한이 있는 대기 (sleep(t), wait(t))
TERMINATED실행 종료

RUNNABLE 안에서 OS가 실제 CPU에 올릴 쓰레드를 고르고, 그 과정에서 컨텍스트 스위칭이 일어난다.


8. 멀티코어 · 병렬성 · 동시성

동시성(Concurrency) vs 병렬성(Parallelism)

  • 동시성: 여러 작업을 번갈아 처리 (코어 1개로도 가능, 컨텍스트 스위칭으로 흉내)
  • 병렬성: 여러 작업을 진짜 동시에 처리 (코어가 여러 개여야 가능)
동시성 (1 core):   A B A B A B   (빠르게 번갈아 → 동시처럼 보임)
병렬성 (2 core):   A A A A       (실제 동시 실행)
                   B B B B

멀티코어에서의 흐름

   ┌── Core 0 ──┐   ┌── Core 1 ──┐
   │ Thread A   │   │ Thread C   │
   │ (실행)      │   │ (실행)     │   ← 진짜 병렬
   └────────────┘   └────────────┘
   Thread B, D ... 는 각 코어에서 컨텍스트 스위칭으로 순번 대기
  • 코어 수만큼 진짜 병렬 실행이 가능.
  • 하이퍼쓰레딩(SMT): 물리 코어 1개를 논리 코어 2개처럼 보이게 해 유휴 자원 활용.
  • CPU 바운드 작업의 최적 쓰레드 수 ≈ 코어 수 (그 이상은 스위칭 오버헤드만 증가).
  • I/O 바운드 작업은 대기가 많으므로 코어 수보다 많은 쓰레드가 유리.

9. 성능 관점 정리

상황권장
CPU 바운드 (계산 위주)쓰레드 수 ≈ 코어 수. 과하게 만들면 스위칭 손해
I/O 바운드 (네트워크/디스크)쓰레드를 더 많이, 또는 가상 쓰레드/비동기 활용
락 경합 심함락 범위 축소, lock-free 자료구조, 분할(sharding)
컨텍스트 스위칭 과다쓰레드 풀 크기 조정, 가상 쓰레드 검토

컨텍스트 스위칭 줄이는 방법

  1. 쓰레드 풀로 쓰레드 수를 적정하게 제한
  2. 불필요한 블로킹/락 최소화
  3. CPU 바운드는 코어 수에 맞추기
  4. I/O 중심이면 비동기(NIO) 또는 가상 쓰레드 활용
  5. 캐시 지역성(locality)을 살리는 데이터 구조/접근 패턴 사용

10. 요약

  • CPU는 Fetch → Decode → Execute → Write-back 사이클을 무한 반복하며 명령을 처리한다.
  • 코어 1개는 한 순간 한 쓰레드만 실행 → 여러 쓰레드를 번갈아 돌리려면 컨텍스트 스위칭 필요.
  • 컨텍스트 스위칭은 실행 중 쓰레드의 상태(PC/레지스터)를 저장하고 다음 쓰레드 상태를 복원하는 작업이며, 순수 오버헤드다. 캐시 오염까지 고려하면 비용이 크다.
  • Java 쓰레드는 (HotSpot 기준) OS 쓰레드와 1:1 매핑 → 스케줄링/스위칭은 OS가 담당.
  • 쓰레드는 Heap을 공유하므로 동기화(synchronized/volatile/atomic) 가 필요하고, 락 대기는 스위칭을 유발한다.
  • Java 21+ 가상 쓰레드는 경량 다중화로 스위칭 비용을 크게 낮춰 I/O 대량 처리에 강하다.
  • 성능의 핵심: 작업 성격(CPU vs I/O)에 맞게 쓰레드 수를 조정해 스위칭 오버헤드를 관리하는 것.

0개의 댓글