4주차 Unit 1.4 — 스케줄링 큐와 컨텍스트 스위칭

Psj·2026년 5월 20일

F-lab

목록 보기
123/240

Unit 1.4 — 스케줄링 큐와 컨텍스트 스위칭

F-LAB JAVA · 4주차 · Phase 1 · 동시성의 기초
🏆 Phase 1 완주 — 동시성의 기초 마스터


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 스케줄링 큐 (Scheduling Queue) 의 정의와 역할은?
  • 컨텍스트 스위칭 (Context Switching) 의 정확한 동작은?
  • 컨텍스트 스위칭의 오버헤드 (직접 + 간접) 는?
  • 스레드를 무한히 만들면 안 되는 이유 는?
  • 코어 4개에 스레드 4개 vs 100개 의 성능 차이는?
  • 레지스터, PC, 스택 포인터 의 저장/복원은?
  • 캐시 미스 (Cache Miss) 가 컨텍스트 스위칭과 어떤 관계인가?
  • 적절한 스레드 수 결정 공식은?
  • Phase 1 전체 의 종합은?

🎯 핵심 한 문장

스케줄링 큐 (Scheduling Queue) 는 OS 내부의 대기 줄로, 실행 가능한 스레드들이 자기 차례를 기다리며, 코어 수만큼 동시 실행된다.
컨텍스트 스위칭 (Context Switching) 은 스레드 A → B 전환 시 A 의 CPU 상태 (레지스터, PC, 스택 포인터) 를 메모리에 저장하고 B 의 이전 상태를 복원하는 작업이다.
이 작업에는 직접 비용 (저장/복원, 수 μs) 과 간접 비용 (캐시 미스, TLB 플러시) 이 발생한다.
스레드를 무한히 만들면 메모리 부족 (스레드당 ~1MB 스택) + 컨텍스트 스위칭 폭증 으로 오히려 느려진다.
코어 4개에 스레드 100개는 4개를 동시 실행하고 나머지는 대기 — 잦은 컨텍스트 스위칭으로 실제 작업 시간보다 전환 오버헤드가 커질 수 있다.

비유 — 은행 창구

스케줄링 큐 = 은행 대기 번호표:
  - 손님들이 번호표 뽑고 대기
  - 창구 (코어) 수만큼 동시 처리
  - 나머지는 줄에서 대기

컨텍스트 스위칭 = 창구 직원 교대:
  - 손님 A 응대 중단
  - A 의 서류 정리해서 보관 (상태 저장)
  - 손님 B 의 서류 꺼냄 (상태 복원)
  - B 응대 시작

오버헤드 = 교대 시간:
  - 서류 정리/꺼내는 시간
  - 너무 자주 교대하면?
  - 실제 응대보다 교대에 시간 더 씀
  - 비효율

무한 스레드 = 무한 손님:
  - 대기실 꽉 참 (메모리)
  - 너무 자주 교대 (스위칭)
  - 처리 느려짐

→ 스케줄링 큐 = 대기 줄, 컨텍스트 스위칭 = 교대 비용.


🧭 9개 섹션 로드맵

1. 스케줄링 큐의 정의와 역할
2. 컨텍스트 스위칭의 동작
3. 저장/복원되는 것 (레지스터, PC 등)
4. 직접 비용 vs 간접 비용
5. 캐시 미스와 컨텍스트 스위칭
6. 스레드를 무한히 만들면
7. 적절한 스레드 수
8. Phase 1 완주 정리
9. 면접 + 자기 점검 + Phase 1 졸업 시험

1️⃣ 스케줄링 큐의 정의와 역할

1.1 스케줄링 큐의 정의

스케줄링 큐 (Scheduling Queue / Ready Queue):

  OS 내부에 존재하는 대기 줄.
  실행 가능한 (RUNNABLE) 스레드들이
  CPU 차례를 기다리는 곳.

역할:
  - 실행 대기 스레드 관리
  - 스케줄러가 다음 스레드 선택
  - 코어 수만큼 동시 실행

1.2 큐의 종류

OS 의 여러 큐:

1. Ready Queue (준비 큐)
   - 실행 가능한 스레드
   - CPU 대기

2. Waiting Queue (대기 큐)
   - I/O 대기 스레드
   - 이벤트 대기

3. 각 I/O 장치별 큐
   - 디스크 큐
   - 네트워크 큐

상태 전이:
  Ready → (CPU 할당) → Running
  Running → (I/O 요청) → Waiting
  Waiting → (I/O 완료) → Ready

1.3 시각화

스케줄링 큐 (4코어 시스템):

Ready Queue:
  [스레드E][스레드F][스레드G][스레드H]...  ← 대기

실행 중 (4코어):
  코어1: 스레드A
  코어2: 스레드B
  코어3: 스레드C
  코어4: 스레드D

  타임 슬라이스 소진 시:
    A → Ready Queue 로
    E → 코어1 에서 실행

  → 순환

1.4 코어 수와 동시 실행

코어 수 = 동시 실행 가능 스레드 수:

코어 1개:
  - 1개만 실행
  - 나머지 대기
  - 멀티태스킹 (번갈아)

코어 4개:
  - 4개 동시 실행
  - 나머지 대기
  - 멀티프로세싱 + 멀티태스킹

핵심:
  - 한 순간 실행 = 코어 수
  - 스레드 100개여도 4개만 동시

1.5 스케줄러의 선택

스케줄러가 다음 스레드 선택:

기준:
  - 우선순위
  - 대기 시간 (공정성)
  - 작업 특성 (CPU/I/O 바운드)

선택 후:
  - Ready Queue 에서 꺼냄
  - 코어에 할당
  - 컨텍스트 스위칭
  - 실행

1.6 ILIC 의 맥락

// ILIC 의 스레드들이 스케줄링 큐에서 대기
// 톰캣 스레드 풀 (200 스레드)

// 200 요청 동시:
// - 4코어면 4개 동시 실행
// - 196개는 Ready Queue 대기
// - 시분할로 번갈아

@RestController
public class ShipmentController {
    
    @GetMapping("/api/shipments/{id}")
    public Shipment get(@PathVariable Long id) {
        // 이 요청 처리 스레드
        // 타임 슬라이스 소진 시 Ready Queue 로
        // DB 조회 (I/O) 시 Waiting Queue 로
        
        return service.findById(id);
        // 처리 완료 후 스레드는 풀로 반환
    }
}

1.7 자기 점검 답변

스케줄링 큐의 정의와 역할은?

:
1. 정의:

  • OS 내부 대기 줄
  • 실행 가능 스레드 대기
  1. 큐 종류:

    • Ready Queue (CPU 대기)
    • Waiting Queue (I/O 대기)
  2. 역할:

    • 스레드 관리
    • 스케줄러가 선택
    • 코어 수만큼 동시
  3. 코어와 관계:

    • 동시 실행 = 코어 수
    • 나머지 대기

2️⃣ 컨텍스트 스위칭의 동작

2.1 컨텍스트 스위칭의 정의

컨텍스트 스위칭 (Context Switching):

  CPU 가 한 스레드에서 다른 스레드로
  전환할 때 발생하는 작업.

동작:
  1. 현재 스레드 (A) 의 상태 저장
  2. 다음 스레드 (B) 의 상태 복원
  3. B 실행 시작

2.2 발생 시점

컨텍스트 스위칭 발생 시점:

1. 타임 슬라이스 소진
   - 할당 시간 끝

2. I/O 요청
   - 스레드가 I/O 대기 → 다른 스레드

3. 우선순위 높은 스레드 도착
   - 선점

4. 스레드 종료
   - 다음 스레드로

5. wait/sleep/yield
   - 자발적 양보

2.3 동작 단계

컨텍스트 스위칭의 상세 동작:

스레드 A → 스레드 B:

1. 스레드 A 중단
   - 현재 명령 완료

2. A 의 컨텍스트 저장 (TCB)
   - PC (Program Counter)
   - 레지스터 값들
   - 스택 포인터
   - 상태 정보

3. 스케줄러가 B 선택

4. B 의 컨텍스트 복원 (TCB)
   - B 의 이전 PC, 레지스터 등

5. 스레드 B 실행
   - B 의 PC 부터 계속

2.4 시각화

컨텍스트 스위칭:

시간 →
  코어:  [스레드A 실행]│←스위칭→│[스레드B 실행]
                      ↑
                  오버헤드 (저장 + 복원)

A 의 상태:
  PC: 0x1000
  레지스터: r1=10, r2=20...
  → TCB_A 에 저장

B 의 상태:
  TCB_B 에서 복원
  PC: 0x5000
  레지스터: r1=30, r2=40...
  → B 의 이전 위치부터

2.5 프로세스 vs 스레드 스위칭

프로세스 컨텍스트 스위칭:
  - PCB 저장/복원
  - 메모리 공간 전환
  - TLB 플러시 (★ 비쌈)
  - 캐시 무효화
  - 비용 큼

스레드 컨텍스트 스위칭 (같은 프로세스):
  - TCB 저장/복원
  - 메모리 공간 유지 (같은 프로세스)
  - TLB 유지 (보통)
  - 비용 적음 (프로세스보다)

핵심:
  - 스레드 스위칭 < 프로세스 스위칭

2.6 ILIC 의 맥락

// 잦은 컨텍스트 스위칭의 영향
public class ContextSwitchingExample {
    
    // ❌ 너무 많은 스레드 (잦은 스위칭)
    public void tooManyThreads() {
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> shortTask()).start();
            // 1만 스레드
            // 4코어면 끊임없이 스위칭
            // 오버헤드 폭증
        }
    }
    
    // ✓ 적절한 스레드 풀
    public void appropriatePool() {
        int cores = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(cores);
        // 코어 수만큼
        // 스위칭 최소화
        
        for (int i = 0; i < 10000; i++) {
            executor.submit(this::shortTask);
        }
        executor.shutdown();
    }
    
    private void shortTask() {
        // 짧은 작업
    }
}

2.7 자기 점검 답변

컨텍스트 스위칭의 동작은?

:
1. 정의:

  • 스레드 전환 작업
  • 상태 저장 + 복원
  1. 발생 시점:

    • 타임 슬라이스 소진
    • I/O 요청
    • 선점
    • wait/sleep/yield
  2. 동작 단계:

    • A 중단 → A 저장 → B 선택 → B 복원 → B 실행
  3. 프로세스 vs 스레드:

    • 프로세스: 메모리 전환 (비쌈)
    • 스레드: 메모리 유지 (적음)

3️⃣ 저장/복원되는 것 (레지스터, PC 등)

3.1 저장되는 컨텍스트

컨텍스트 스위칭 시 저장/복원:

1. PC (Program Counter)
   - 다음 실행할 명령 주소
   - 어디까지 실행했는지

2. 레지스터 (Registers)
   - CPU 의 작업 공간
   - 계산 중인 값들
   - 범용 레지스터

3. 스택 포인터 (Stack Pointer)
   - 현재 스택 위치

4. 상태 레지스터 (Status Register)
   - 플래그 (zero, carry 등)

5. 기타
   - 메모리 관리 정보

3.2 PC (Program Counter)

PC (Program Counter):

  다음에 실행할 명령의 주소.

역할:
  - 어디까지 실행했는지 기억
  - 복원 시 그 위치부터 계속

예:
  스레드 A 가 line 50 실행 중 중단
  → PC = line 50 저장
  → 나중에 복원 시 line 50 부터

3.3 레지스터

레지스터 (Registers):

  CPU 안의 초고속 저장 공간.

종류:
  - 범용 레지스터 (계산용)
  - 특수 레지스터 (PC, SP 등)

저장 이유:
  - 계산 중인 값들
  - 스레드 B 가 덮어쓰기 전에 저장
  - 복원 시 A 의 계산 이어감

3.4 시각화

저장/복원:

스레드 A (중단 직전):
  PC: 0x4050
  r1: 100
  r2: 200
  SP: 0x7fff...

  ↓ 저장 (TCB_A)

[스레드 B 실행 — 레지스터 덮어씀]
  PC: 0x8090
  r1: 999
  r2: 888

  ↓ A 복원 (TCB_A)

스레드 A (재개):
  PC: 0x4050  ← 복원
  r1: 100     ← 복원
  r2: 200
  → 중단 지점부터 계속

3.5 자바와 컨텍스트

// 자바 스레드의 상태는 OS 가 관리
// 컨텍스트 스위칭은 OS 레벨

Thread t = new Thread(() -> {
    int x = 10;        // 레지스터/스택
    process(x);        // PC 이동
    // 여기서 스위칭 발생 가능
    // x, PC 등 저장됨
    int y = x + 5;     // 복원 후 계속
});

// 자바 개발자는 직접 컨텍스트 관리 X
// OS 가 처리
// 단, 스레드 수 조절로 스위칭 빈도 영향

3.6 ThreadLocal 과 컨텍스트

// ThreadLocal 은 컨텍스트 스위칭과 무관하게 유지
public class ContextExample {
    
    private static final ThreadLocal<String> context = new ThreadLocal<>();
    
    public void process() {
        context.set("requestId-123");
        
        doWork();   // 컨텍스트 스위칭 발생해도
        // context 값은 유지 (스레드별 격리)
        
        String value = context.get();   // "requestId-123"
        // 스위칭과 무관하게 같은 스레드면 유지
    }
}

3.7 자기 점검 답변

저장/복원되는 것은?

:
1. PC:

  • 다음 명령 주소
  • 실행 위치
  1. 레지스터:

    • 계산 중인 값
    • CPU 작업 공간
  2. 스택 포인터:

    • 스택 위치
  3. 상태 레지스터:

    • 플래그
  4. 자바:

    • OS 가 관리
    • 개발자는 스레드 수만 조절

4️⃣ 직접 비용 vs 간접 비용

4.1 두 가지 비용

컨텍스트 스위칭의 비용:

직접 비용 (Direct Cost):
  - 레지스터 저장/복원
  - 스케줄러 실행
  - 측정 가능 (수 μs)

간접 비용 (Indirect Cost):
  - 캐시 미스
  - TLB 플러시
  - 파이프라인 비우기
  - 측정 어려움, 더 클 수 있음

4.2 직접 비용

직접 비용:

  - 현재 레지스터 저장 (메모리 쓰기)
  - 다음 레지스터 복원 (메모리 읽기)
  - 스케줄러 알고리즘 실행
  - TCB 업데이트

크기:
  - 약 1~10 μs (마이크로초)
  - 비교적 작고 고정적

4.3 간접 비용

간접 비용 (더 큼):

1. 캐시 미스
   - 스레드 A 의 데이터가 캐시에
   - B 실행 → B 데이터로 캐시 채움
   - A 재개 시 → 캐시 미스 (다시 로드)

2. TLB 플러시 (프로세스 전환 시)
   - 가상 주소 변환 캐시
   - 무효화 → 다시 채움

3. 파이프라인 비우기
   - CPU 파이프라인 초기화

크기:
  - 직접 비용보다 클 수 있음
  - 캐시 미스가 주요 원인

4.4 시각화 — 캐시 영향

캐시와 컨텍스트 스위칭:

스레드 A 실행:
  CPU 캐시: [A 의 데이터로 채워짐]
  빠른 접근

컨텍스트 스위칭 → 스레드 B:
  CPU 캐시: [B 의 데이터로 교체]
  A 데이터 밀려남

A 재개:
  CPU 캐시: A 데이터 없음 (캐시 미스)
  메인 메모리에서 다시 로드 (느림)
  → 간접 비용

4.5 비용의 누적

잦은 스위칭의 누적 비용:

작업 시간 10ms, 스위칭 비용 0.1ms:
  - 스위칭 10번: 1ms 오버헤드 (10%)
  - 스위칭 100번: 10ms 오버헤드 (100%!)

너무 잦으면:
  - 실제 작업 < 스위칭 오버헤드
  - 처리량 ↓
  - 비효율

4.6 측정

# Linux 에서 컨텍스트 스위칭 측정
$ vmstat 1
# cs 컬럼 = context switches per second

$ pidstat -w 1
# 프로세스별 컨텍스트 스위칭

# 자바
# JFR (Java Flight Recorder)
# 스레드 상태, 스위칭 분석

4.7 ILIC 의 맥락

public class ContextSwitchCost {
    
    // 측정 — 너무 많은 스레드의 영향
    public void measureImpact() {
        int cores = Runtime.getRuntime().availableProcessors();
        
        // 케이스 1: 코어 수만큼 (효율)
        long t1 = benchmark(cores);
        
        // 케이스 2: 코어의 100배 (비효율)
        long t2 = benchmark(cores * 100);
        
        System.out.println("Optimal: " + t1 + "ms");
        System.out.println("Too many: " + t2 + "ms");
        // t2 가 t1 보다 느릴 수 있음 (스위칭 오버헤드)
    }
    
    private long benchmark(int threadCount) {
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        long start = System.currentTimeMillis();
        
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            futures.add(executor.submit(this::cpuTask));
        }
        futures.forEach(this::await);
        
        executor.shutdown();
        return System.currentTimeMillis() - start;
    }
    
    private void cpuTask() {
        // CPU 작업
        long sum = 0;
        for (int i = 0; i < 1000; i++) sum += i;
    }
    
    private void await(Future<?> f) {
        try { f.get(); } catch (Exception e) { }
    }
}

4.8 자기 점검 답변

직접 비용 vs 간접 비용은?

:
1. 직접 비용:

  • 레지스터 저장/복원
  • 스케줄러
  • 수 μs
  1. 간접 비용:

    • 캐시 미스 (주요)
    • TLB 플러시
    • 파이프라인
    • 더 클 수 있음
  2. 누적:

    • 잦으면 작업 시간 초과
    • 처리량 ↓
  3. 측정:

    • vmstat (cs)
    • JFR

5️⃣ 캐시 미스와 컨텍스트 스위칭

5.1 CPU 캐시 계층

CPU 캐시 계층:

레지스터 (가장 빠름, 작음)
   ↓
L1 캐시 (~1ns, 수십 KB)
   ↓
L2 캐시 (~4ns, 수백 KB)
   ↓
L3 캐시 (~10ns, 수 MB, 공유)
   ↓
메인 메모리 (~100ns, GB)
   ↓
디스크 (~10ms, TB)

속도 차이:
  - 캐시 ↔ 메모리: 약 100배
  - 캐시 미스 = 큰 비용

5.2 캐시 미스의 발생

컨텍스트 스위칭 → 캐시 미스:

1. 스레드 A 실행 중
   - A 의 데이터가 캐시에
   - 빠른 접근

2. 컨텍스트 스위칭 → 스레드 B
   - B 의 데이터를 캐시로
   - A 데이터 밀려남 (eviction)

3. 스레드 A 재개
   - A 데이터가 캐시에 없음
   - 캐시 미스
   - 메인 메모리에서 다시 (느림)

5.3 캐시 지역성

캐시 지역성 (Locality):

시간 지역성 (Temporal):
  - 최근 접근한 데이터 다시 접근
  - 캐시에 유지하면 효율

공간 지역성 (Spatial):
  - 인접 메모리 함께 접근
  - 캐시 라인 단위 로드

컨텍스트 스위칭:
  - 지역성 깨뜨림
  - 캐시 효율 ↓

5.4 캐시 친화적 설계

// 캐시 친화적 — 스레드 수 최소화
public class CacheFriendly {
    
    // ✓ 코어 수만큼 (캐시 효율)
    public void process(List<Task> tasks) {
        int cores = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(cores);
        // 코어당 1 스레드 → 캐시 유지 ↑
        
        tasks.forEach(t -> executor.submit(() -> t.run()));
        executor.shutdown();
    }
    
    // ✓ CPU 친화도 (Affinity) — 고급
    // 특정 스레드를 특정 코어에 고정
    // 캐시 유지 (자바 표준엔 없음, JNI 등 필요)
}

5.5 false sharing

// false sharing — 캐시 라인 경합
public class FalseSharing {
    
    // ❌ 같은 캐시 라인에 두 변수
    static class Counters {
        volatile long count1;   // 스레드 A 가 수정
        volatile long count2;   // 스레드 B 가 수정
        // count1, count2 가 같은 캐시 라인 (보통 64바이트)
        // A 가 count1 수정 → 캐시 라인 무효화
        // B 의 count2 도 영향 (실제 공유 안 하는데)
        // → false sharing
    }
    
    // ✓ 패딩으로 분리 (@Contended 또는 수동)
    static class PaddedCounters {
        volatile long count1;
        long p1, p2, p3, p4, p5, p6, p7;   // 패딩
        volatile long count2;
        // 다른 캐시 라인 → false sharing 방지
    }
}

5.6 ILIC 의 맥락

public class CacheAwareProcessing {
    
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    // 캐시 친화적 배치 처리
    public void processBatch(List<Shipment> shipments) {
        // 코어 수만큼 스레드 (캐시 효율)
        ExecutorService executor = Executors.newFixedThreadPool(cores);
        
        // 데이터를 코어 수만큼 청크로 분할
        int chunkSize = (shipments.size() + cores - 1) / cores;
        
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < shipments.size(); i += chunkSize) {
            int end = Math.min(i + chunkSize, shipments.size());
            List<Shipment> chunk = shipments.subList(i, end);
            
            futures.add(executor.submit(() -> {
                // 각 스레드가 연속된 청크 처리
                // 캐시 지역성 ↑
                chunk.forEach(this::process);
            }));
        }
        
        futures.forEach(this::await);
        executor.shutdown();
    }
    
    private void process(Shipment s) { }
    private void await(Future<?> f) {
        try { f.get(); } catch (Exception e) { }
    }
}

5.7 자기 점검 답변

캐시 미스와 컨텍스트 스위칭의 관계는?

:
1. 캐시 계층:

  • 레지스터 → L1/L2/L3 → 메모리
  • 캐시 미스 = 약 100배 느림
  1. 스위칭 → 캐시 미스:

    • A 데이터 밀려남
    • A 재개 시 미스
    • 메모리 재로드
  2. 지역성:

    • 시간/공간 지역성
    • 스위칭이 깨뜨림
  3. false sharing:

    • 같은 캐시 라인 경합
    • 패딩으로 방지

6️⃣ 스레드를 무한히 만들면

6.1 두 가지 문제

스레드 무한 생성의 문제:

1. 메모리 부족
   - 스레드당 ~1MB 스택
   - 1만 스레드 = ~10GB
   - OutOfMemoryError

2. 컨텍스트 스위칭 폭증
   - 코어보다 많으면 잦은 스위칭
   - 오버헤드 누적
   - 성능 저하

6.2 메모리 문제

메모리 계산:

스레드 1개:
  - 스택 ~1MB (-Xss 기본)
  - TCB 등 추가

1,000 스레드:
  - 약 1GB 스택

10,000 스레드:
  - 약 10GB 스택
  - 일반 서버 메모리 초과
  - OutOfMemoryError: unable to create new native thread

6.3 스위칭 문제

컨텍스트 스위칭 폭증:

코어 4개, 스레드 1000개:
  - 4개만 동시 실행
  - 996개 대기
  - 끊임없는 스위칭

  타임 슬라이스 10ms:
    - 1000개 한 바퀴 = 10초
    - 각 스레드 매우 가끔 실행
    - 스위칭 오버헤드 누적

결과:
  - 실제 작업 < 스위칭
  - 처리량 급감

6.4 스레싱 (Thrashing)

스레싱 (Thrashing):

  과도한 컨텍스트 스위칭으로
  실제 작업보다 전환에 더 많은 시간.

증상:
  - CPU 사용률 높지만
  - 실제 처리량 낮음
  - 시스템 느려짐

원인:
  - 너무 많은 스레드/프로세스
  - 메모리 부족 (페이지 스와핑)

6.5 해결 — 스레드 풀

// ❌ 무한 생성
public void badPattern(List<Task> tasks) {
    for (Task task : tasks) {
        new Thread(task).start();   // 작업마다 새 스레드
        // 100만 작업 = 100만 스레드 → 폭발
    }
}

// ✓ 스레드 풀 (재사용)
public void goodPattern(List<Task> tasks) {
    int cores = Runtime.getRuntime().availableProcessors();
    ExecutorService executor = Executors.newFixedThreadPool(cores);
    // 고정 스레드, 재사용
    
    tasks.forEach(executor::submit);
    executor.shutdown();
    // 스레드 수 제한 → 스위칭 최소화
}

6.6 ILIC 의 맥락

// ❌ 위험한 패턴
@Service
public class ShipmentBadService {
    
    public void processAll(List<Shipment> shipments) {
        for (Shipment s : shipments) {
            new Thread(() -> process(s)).start();
            // 수만 건 = 수만 스레드 → OOM
        }
    }
    
    private void process(Shipment s) { }
}

// ✓ 안전한 패턴
@Service
public class ShipmentGoodService {
    
    private final ExecutorService executor;
    
    public ShipmentGoodService() {
        int cores = Runtime.getRuntime().availableProcessors();
        this.executor = Executors.newFixedThreadPool(cores);
    }
    
    public void processAll(List<Shipment> shipments) {
        shipments.forEach(s -> executor.submit(() -> process(s)));
        // 고정 스레드 풀
        // 스위칭 최소화
    }
    
    private void process(Shipment s) { }
    
    @PreDestroy
    public void shutdown() {
        executor.shutdown();
    }
}

6.7 자기 점검 답변

스레드를 무한히 만들면 안 되는 이유는?

:
1. 메모리 부족:

  • 스레드당 ~1MB 스택
  • 1만 = 10GB
  • OOM
  1. 스위칭 폭증:

    • 코어보다 많으면
    • 잦은 전환
    • 오버헤드 누적
  2. 스레싱:

    • 작업 < 스위칭
    • 처리량 급감
  3. 해결:

    • 스레드 풀
    • 재사용
    • 수 제한

7️⃣ 적절한 스레드 수

7.1 코어 4개에 4 vs 100

코어 4개 시나리오:

스레드 4개 (= 코어 수):
  - 4개 모두 동시 실행
  - 스위칭 최소
  - CPU 바운드에 최적

스레드 100개:
  - 4개만 동시
  - 96개 대기
  - 잦은 스위칭
  - CPU 바운드면 비효율 (4개와 비슷하거나 느림)
  - I/O 바운드면 효율 (대기 동안 다른 스레드)

7.2 CPU 바운드 공식

CPU 바운드 작업:

이상적 스레드 수 = 코어 수

이유:
  - 계산 위주
  - 모든 코어 풀가동
  - 더 많아도 동시 실행 X
  - 스위칭만 늘어남

예:
  4코어 → 4 스레드
  이미지 처리, 암호화, 계산

7.3 I/O 바운드 공식

I/O 바운드 작업:

이상적 스레드 수 = 코어 수 × (1 + 대기시간/작업시간)

이유:
  - I/O 대기 동안 다른 스레드
  - 코어보다 많이 필요

예:
  4코어, 작업 10ms, I/O 대기 90ms:
  4 × (1 + 90/10) = 4 × 10 = 40 스레드

  DB 조회, API 호출, 파일

7.4 Little's Law

Little's Law:

  스레드 수 = 처리율 × 응답 시간

또는:
  L = λ × W
  - L: 시스템 내 평균 작업 수
  - λ: 도착률
  - W: 평균 처리 시간

활용:
  - 목표 처리율 달성에 필요한 스레드
  - 측정 기반 튜닝

7.5 측정 기반 결정

public class ThreadPoolSizing {
    
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    // CPU 바운드 — 코어 수
    public ExecutorService cpuBoundPool() {
        return Executors.newFixedThreadPool(cores);
    }
    
    // I/O 바운드 — 더 많이
    public ExecutorService ioBoundPool() {
        // 측정 후 조정 (예: 대기시간/작업시간 = 4)
        int poolSize = cores * (1 + 4);   // = cores * 5
        return Executors.newFixedThreadPool(poolSize);
    }
    
    // 혼합 — 별도 풀
    public void mixedWorkload() {
        ExecutorService cpuPool = cpuBoundPool();
        ExecutorService ioPool = ioBoundPool();
        // 작업 유형에 따라 분리
    }
}

7.6 Virtual Thread (Java 21+)

// Virtual Thread — I/O 바운드 혁신
public class VirtualThreadExample {
    
    // 수만~수백만 가상 스레드 가능
    public void manyIoTasks(List<Long> ids) {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (Long id : ids) {
                executor.submit(() -> {
                    // I/O 작업 (DB, API)
                    fetchFromDb(id);
                    // 가상 스레드는 I/O 대기 시 OS 스레드 양보
                    // 컨텍스트 스위칭 비용 ↓
                });
            }
        }
        // 100만 가상 스레드도 OK (메모리 적음)
    }
    
    private void fetchFromDb(Long id) { }
}

// Virtual Thread:
// - 경량 (수 KB)
// - I/O 대기 시 자동 양보
// - 컨텍스트 스위칭 효율
// - 무한 생성 문제 완화

7.7 ILIC 의 맥락

@Configuration
public class ThreadPoolConfig {
    
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    // CPU 바운드 — 운임 계산
    @Bean("freightCalculator")
    public ExecutorService freightCalculatorPool() {
        return Executors.newFixedThreadPool(cores);
    }
    
    // I/O 바운드 — 외부 API
    @Bean("trackingApi")
    public ExecutorService trackingApiPool() {
        return Executors.newFixedThreadPool(cores * 5);
        // API 호출 대기 많음
    }
    
    // 대량 I/O — Virtual Thread (Java 21+)
    @Bean("bulkProcessor")
    public ExecutorService bulkProcessorPool() {
        return Executors.newVirtualThreadPerTaskExecutor();
        // 수만 건 동시 조회
    }
}

7.8 자기 점검 답변

적절한 스레드 수는?

:
1. CPU 바운드:

  • 코어 수
  • 더 많아도 스위칭만
  1. I/O 바운드:

    • 코어 수 × (1 + 대기/작업)
    • 대기 동안 다른 스레드
  2. Little's Law:

    • 스레드 수 = 처리율 × 응답시간
  3. 4코어 4 vs 100:

    • CPU: 4 최적, 100 비효율
    • I/O: 100 이 효율
  4. Virtual Thread:

    • I/O 바운드 혁신
    • 수만 개 가능

8️⃣ Phase 1 완주 정리

8.1 Phase 1 학습 종합

Phase 1 — 동시성의 기초

Unit 1.1 — 멀티태스킹 vs 멀티프로세싱
  - 멀티태스킹: 1코어 번갈아 (동시성)
  - 멀티프로세싱: 여러 코어 동시 (병렬성)
  - 시분할, 스케줄링

Unit 1.2 — 프로세스와 스레드
  - 프로세스: 독립 메모리
  - 스레드: 실행 흐름
  - 공유 (힙) vs 개별 (스택)

Unit 1.3 — 변수 종류와 메모리 위치
  - 지역/매개변수: Stack (안전)
  - 인스턴스: Heap (조건부)
  - static: Method Area (공유)
  - "스레드 안전 = 공유되는가?"

Unit 1.4 — 스케줄링 큐와 컨텍스트 스위칭
  - 스케줄링 큐
  - 컨텍스트 스위칭 (저장/복원)
  - 직접/간접 비용
  - 적절한 스레드 수

8.2 Phase 1 의 큰 그림

동시성의 기초:

1. 동시 실행의 두 방식
   - 멀티태스킹 (동시성)
   - 멀티프로세싱 (병렬성)

2. 실행 단위
   - 프로세스 (독립)
   - 스레드 (공유 + 흐름)

3. 메모리와 안전성
   - 공유 = 위험
   - 개별 = 안전

4. 비용
   - 컨텍스트 스위칭
   - 적절한 스레드 수

핵심:
  - 공유 메모리 → 동시성 문제
  - Phase 4 부터 해결

8.3 다음 Phase 예고

Phase 1 → Phase 2:
  - 기초 → 4분면 매트릭스
  - Sync/Async × Blocking/Non-Blocking

이후:
  - Phase 3: 스레드 만들기
  - Phase 4: synchronized (동기화)
  - Phase 5: Lock
  - Phase 6: 협력
  - Phase 7: Executor
  - Phase 8: 고급 비동기

8.4 Phase 1 핵심 통찰

Phase 1 의 핵심 통찰 5가지:

1. 동시성 ≠ 병렬성
   - 번갈아 vs 진짜 동시

2. 공유가 문제의 근원
   - 힙 공유 → 동시성 문제

3. 스레드 안전 = 공유 여부
   - 지역 변수는 안전

4. 컨텍스트 스위칭은 비용
   - 무한 스레드 X

5. 작업 유형별 스레드 수
   - CPU: 코어 수
   - I/O: 더 많이

8.5 4주차 누적 진행

✅ Phase 1 — 동시성의 기초 (4 Unit) ← 완주
⏭ Phase 2 — 4분면 매트릭스 (3 Unit)
⏭ Phase 3 — 스레드 다루기 (5 Unit)
⏭ Phase 4 — synchronized & volatile (5 Unit)
⏭ Phase 5 — Lock 도구 (4 Unit)
⏭ Phase 6 — 스레드 협력 (4 Unit)
⏭ Phase 7 — Executor (7 Unit)
⏭ Phase 8 — 고급 비동기 (3 Unit)

총: 4/35 Unit (Phase 1 완주, 약 11%)

8.6 자기 점검 답변

Phase 1 의 종합은?

:
1. 4개 Unit:

  • 멀티태스킹/프로세싱
  • 프로세스/스레드
  • 변수/메모리
  • 스케줄링/스위칭
  1. 큰 그림:

    • 동시 실행 방식
    • 실행 단위
    • 메모리와 안전성
    • 비용
  2. 핵심 통찰:

    • 동시성 ≠ 병렬성
    • 공유 = 문제 근원
    • 안전 = 공유 여부
    • 스위칭 비용

9️⃣ 면접 + 자기 점검 + Phase 1 졸업 시험

9.1 면접 단골 질문 매핑

Q핵심 답변
스케줄링 큐?실행 대기 스레드 줄
컨텍스트 스위칭?스레드 전환 (저장/복원)
저장되는 것?PC, 레지스터, 스택 포인터
직접 비용?레지스터 저장/복원 (μs)
간접 비용?캐시 미스, TLB 플러시
무한 스레드 문제?메모리 + 스위칭 폭증
4코어 4 vs 100?CPU: 4 최적, I/O: 100 효율
CPU 바운드 스레드 수?코어 수
I/O 바운드 스레드 수?코어 × (1 + 대기/작업)
스레싱?작업 < 스위칭

9.2 Phase 1 졸업 시험 50문항

멀티태스킹/멀티프로세싱 (12문항)

Q1. 멀티태스킹? → 1코어 번갈아
Q2. 멀티프로세싱? → 여러 코어 동시
Q3. 시분할? → CPU 시간 쪼개 할당
Q4. 동시성? → 번갈아 (논리)
Q5. 병렬성? → 동시 (물리)
Q6. 단일 코어 동시 실행? → 멀티태스킹 환상
Q7. 함께 사용? → 가능 (현대 표준)
Q8. 타임 슬라이스? → 작업당 CPU 시간
Q9. 선점형? → OS 강제 전환
Q10. 하이퍼스레딩? → 1코어 2논리
Q11. availableProcessors? → 코어 수
Q12. Rob Pike? → 동시성 dealing, 병렬성 doing

프로세스/스레드 (13문항)

Q13. 프로세스? → 실행 중 프로그램
Q14. 스레드? → 실행 흐름
Q15. 프로그램 vs 프로세스? → 정적 vs 실행 중
Q16. 메모리 4영역? → 코드/데이터/힙/스택
Q17. 공유? → 코드/데이터/힙
Q18. 개별? → 스택
Q19. 스택 개별 이유? → 독립 실행
Q20. 힙 공유 이유? → 협력
Q21. IPC? → 프로세스 간 통신
Q22. PCB? → 프로세스 관리 정보
Q23. TCB? → 스레드 관리 정보
Q24. 멀티프로세스 장점? → 안정성, 격리
Q25. 멀티스레드 장점? → 효율, 통신

변수/메모리 (13문항)

Q26. 지역 변수 위치? → Stack
Q27. 인스턴스 변수 위치? → Heap
Q28. static 변수 위치? → Method Area
Q29. 지역 변수 안전? → 안전 (스레드별)
Q30. 인스턴스 안전? → 조건부 (객체 공유)
Q31. static 안전? → 항상 공유 (위험)
Q32. 스레드 안전 = ? → 공유되는가?
Q33. 무상태? → 가변 필드 없음 (안전)
Q34. 매개변수 안전? → 기본 안전, 참조 주의
Q35. 참조 vs 객체? → 참조 스택, 객체 힙
Q36. 불변 객체 안전? → 변경 X
Q37. record? → 불변 (Java 16+)
Q38. Spring 싱글톤 위험? → 가변 인스턴스 변수

스케줄링/스위칭 (12문항)

Q39. 스케줄링 큐? → 대기 줄
Q40. 컨텍스트 스위칭? → 스레드 전환
Q41. 저장되는 것? → PC, 레지스터, SP
Q42. 직접 비용? → 저장/복원 (μs)
Q43. 간접 비용? → 캐시 미스
Q44. 캐시 계층? → 레지스터 → L1/L2/L3 → 메모리
Q45. 무한 스레드 문제? → 메모리 + 스위칭
Q46. 스레싱? → 작업 < 스위칭
Q47. CPU 바운드 스레드? → 코어 수
Q48. I/O 바운드 스레드? → 더 많이
Q49. Little's Law? → 처리율 × 응답시간
Q50. Virtual Thread? → 경량, I/O 효율

9.3 채점

50 / 50 → Phase 1 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 1.1 ~ 1.4 재학습

9.4 추가 심화 질문

Q1: jstack 으로 무엇을 볼 수 있나?

답:

  • 스레드 덤프
  • 각 스레드 상태 (BLOCKED, WAITING 등)
  • 스택 트레이스
  • 데드락 감지
  • 성능 문제 진단

Q2: 컨텍스트 스위칭을 줄이는 방법?

답:

  • 적절한 스레드 수 (코어 수 근처)
  • 스레드 풀 사용
  • CPU 친화도 (affinity)
  • Virtual Thread (I/O)
  • 락 경합 줄이기

Q3: 협력적 vs 선점형 스케줄링?

답:

  • 협력적: 작업이 자발적 양보 (yield)
  • 선점형: OS 강제 (타임 슬라이스)
  • 현대 OS: 선점형
  • 자바 yield: 협력적 힌트

Q4: 스레드 우선순위가 보장 안 되는 이유?

답:

  • OS 마다 다름
  • 자바 우선순위 1~10
  • OS 우선순위로 매핑 (불완전)
  • 힌트일 뿐
  • 의존하면 안 됨

Q5: 데몬 스레드와 컨텍스트 스위칭?

답:

  • 데몬도 일반 스레드처럼 스위칭
  • GC 스레드 등 (데몬)
  • 백그라운드라도 CPU 사용
  • 모든 일반 스레드 종료 시 데몬도 종료

🎯 핵심 요약 — 3줄 정리

1. 스케줄링 큐 + 컨텍스트 스위칭

  • 큐: 실행 대기 스레드 줄
  • 스위칭: 전환 (PC, 레지스터 저장/복원)

2. 비용

  • 직접: 저장/복원 (μs)
  • 간접: 캐시 미스 (더 큼)

3. 적절한 스레드 수

  • CPU 바운드: 코어 수
  • I/O 바운드: 더 많이
  • 무한 생성 X (스레드 풀)

🏆 Phase 1 완주 — 동시성의 기초 마스터

🚀 Phase 1 — 동시성의 기초
  ✅ Unit 1.1 멀티태스킹 vs 멀티프로세싱
  ✅ Unit 1.2 프로세스와 스레드
  ✅ Unit 1.3 변수 종류와 메모리 위치
  ✅ Unit 1.4 스케줄링 큐와 컨텍스트 스위칭 ← 여기, Phase 1 완주

→ 동시성 vs 병렬성
→ 프로세스/스레드 메모리
→ 스레드 안전 = 공유 여부
→ 컨텍스트 스위칭 비용

📚 다음으로...

Phase 2 — Sync/Async × Blocking/Non-Blocking 4분면

Phase 2 — 4분면 매트릭스 (3 Unit)

Unit 2.1 — Sync vs Async (작업 순서 관점)
Unit 2.2 — Blocking vs Non-Blocking (제어권 관점)
Unit 2.3 — 4분면 매트릭스 (★ 마스터, 면접 단골)

4주차 누적 진행

✅ Phase 1 — 동시성의 기초 (4 Unit)
⏭ Phase 2 — 4분면 매트릭스 (3 Unit)

총: 4/35 Unit (Phase 1 완주, 약 11%)
profile
Software Developer

0개의 댓글