F-LAB JAVA · 4주차 · Phase 1 · 동시성의 기초
🏆 Phase 1 완주 — 동시성의 기초 마스터
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
스케줄링 큐 (Scheduling Queue) 는 OS 내부의 대기 줄로, 실행 가능한 스레드들이 자기 차례를 기다리며, 코어 수만큼 동시 실행된다.
컨텍스트 스위칭 (Context Switching) 은 스레드 A → B 전환 시 A 의 CPU 상태 (레지스터, PC, 스택 포인터) 를 메모리에 저장하고 B 의 이전 상태를 복원하는 작업이다.
이 작업에는 직접 비용 (저장/복원, 수 μs) 과 간접 비용 (캐시 미스, TLB 플러시) 이 발생한다.
스레드를 무한히 만들면 메모리 부족 (스레드당 ~1MB 스택) + 컨텍스트 스위칭 폭증 으로 오히려 느려진다.
코어 4개에 스레드 100개는 4개를 동시 실행하고 나머지는 대기 — 잦은 컨텍스트 스위칭으로 실제 작업 시간보다 전환 오버헤드가 커질 수 있다.
스케줄링 큐 = 은행 대기 번호표:
- 손님들이 번호표 뽑고 대기
- 창구 (코어) 수만큼 동시 처리
- 나머지는 줄에서 대기
컨텍스트 스위칭 = 창구 직원 교대:
- 손님 A 응대 중단
- A 의 서류 정리해서 보관 (상태 저장)
- 손님 B 의 서류 꺼냄 (상태 복원)
- B 응대 시작
오버헤드 = 교대 시간:
- 서류 정리/꺼내는 시간
- 너무 자주 교대하면?
- 실제 응대보다 교대에 시간 더 씀
- 비효율
무한 스레드 = 무한 손님:
- 대기실 꽉 참 (메모리)
- 너무 자주 교대 (스위칭)
- 처리 느려짐
→ 스케줄링 큐 = 대기 줄, 컨텍스트 스위칭 = 교대 비용.
1. 스케줄링 큐의 정의와 역할
2. 컨텍스트 스위칭의 동작
3. 저장/복원되는 것 (레지스터, PC 등)
4. 직접 비용 vs 간접 비용
5. 캐시 미스와 컨텍스트 스위칭
6. 스레드를 무한히 만들면
7. 적절한 스레드 수
8. Phase 1 완주 정리
9. 면접 + 자기 점검 + Phase 1 졸업 시험
스케줄링 큐 (Scheduling Queue / Ready Queue):
OS 내부에 존재하는 대기 줄.
실행 가능한 (RUNNABLE) 스레드들이
CPU 차례를 기다리는 곳.
역할:
- 실행 대기 스레드 관리
- 스케줄러가 다음 스레드 선택
- 코어 수만큼 동시 실행
OS 의 여러 큐:
1. Ready Queue (준비 큐)
- 실행 가능한 스레드
- CPU 대기
2. Waiting Queue (대기 큐)
- I/O 대기 스레드
- 이벤트 대기
3. 각 I/O 장치별 큐
- 디스크 큐
- 네트워크 큐
상태 전이:
Ready → (CPU 할당) → Running
Running → (I/O 요청) → Waiting
Waiting → (I/O 완료) → Ready
스케줄링 큐 (4코어 시스템):
Ready Queue:
[스레드E][스레드F][스레드G][스레드H]... ← 대기
실행 중 (4코어):
코어1: 스레드A
코어2: 스레드B
코어3: 스레드C
코어4: 스레드D
타임 슬라이스 소진 시:
A → Ready Queue 로
E → 코어1 에서 실행
→ 순환
코어 수 = 동시 실행 가능 스레드 수:
코어 1개:
- 1개만 실행
- 나머지 대기
- 멀티태스킹 (번갈아)
코어 4개:
- 4개 동시 실행
- 나머지 대기
- 멀티프로세싱 + 멀티태스킹
핵심:
- 한 순간 실행 = 코어 수
- 스레드 100개여도 4개만 동시
스케줄러가 다음 스레드 선택:
기준:
- 우선순위
- 대기 시간 (공정성)
- 작업 특성 (CPU/I/O 바운드)
선택 후:
- Ready Queue 에서 꺼냄
- 코어에 할당
- 컨텍스트 스위칭
- 실행
// 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. 정의:
큐 종류:
역할:
코어와 관계:
컨텍스트 스위칭 (Context Switching):
CPU 가 한 스레드에서 다른 스레드로
전환할 때 발생하는 작업.
동작:
1. 현재 스레드 (A) 의 상태 저장
2. 다음 스레드 (B) 의 상태 복원
3. B 실행 시작
컨텍스트 스위칭 발생 시점:
1. 타임 슬라이스 소진
- 할당 시간 끝
2. I/O 요청
- 스레드가 I/O 대기 → 다른 스레드
3. 우선순위 높은 스레드 도착
- 선점
4. 스레드 종료
- 다음 스레드로
5. wait/sleep/yield
- 자발적 양보
컨텍스트 스위칭의 상세 동작:
스레드 A → 스레드 B:
1. 스레드 A 중단
- 현재 명령 완료
2. A 의 컨텍스트 저장 (TCB)
- PC (Program Counter)
- 레지스터 값들
- 스택 포인터
- 상태 정보
3. 스케줄러가 B 선택
4. B 의 컨텍스트 복원 (TCB)
- B 의 이전 PC, 레지스터 등
5. 스레드 B 실행
- B 의 PC 부터 계속
컨텍스트 스위칭:
시간 →
코어: [스레드A 실행]│←스위칭→│[스레드B 실행]
↑
오버헤드 (저장 + 복원)
A 의 상태:
PC: 0x1000
레지스터: r1=10, r2=20...
→ TCB_A 에 저장
B 의 상태:
TCB_B 에서 복원
PC: 0x5000
레지스터: r1=30, r2=40...
→ B 의 이전 위치부터
프로세스 컨텍스트 스위칭:
- PCB 저장/복원
- 메모리 공간 전환
- TLB 플러시 (★ 비쌈)
- 캐시 무효화
- 비용 큼
스레드 컨텍스트 스위칭 (같은 프로세스):
- TCB 저장/복원
- 메모리 공간 유지 (같은 프로세스)
- TLB 유지 (보통)
- 비용 적음 (프로세스보다)
핵심:
- 스레드 스위칭 < 프로세스 스위칭
// 잦은 컨텍스트 스위칭의 영향
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() {
// 짧은 작업
}
}
컨텍스트 스위칭의 동작은?
답:
1. 정의:
발생 시점:
동작 단계:
프로세스 vs 스레드:
컨텍스트 스위칭 시 저장/복원:
1. PC (Program Counter)
- 다음 실행할 명령 주소
- 어디까지 실행했는지
2. 레지스터 (Registers)
- CPU 의 작업 공간
- 계산 중인 값들
- 범용 레지스터
3. 스택 포인터 (Stack Pointer)
- 현재 스택 위치
4. 상태 레지스터 (Status Register)
- 플래그 (zero, carry 등)
5. 기타
- 메모리 관리 정보
PC (Program Counter):
다음에 실행할 명령의 주소.
역할:
- 어디까지 실행했는지 기억
- 복원 시 그 위치부터 계속
예:
스레드 A 가 line 50 실행 중 중단
→ PC = line 50 저장
→ 나중에 복원 시 line 50 부터
레지스터 (Registers):
CPU 안의 초고속 저장 공간.
종류:
- 범용 레지스터 (계산용)
- 특수 레지스터 (PC, SP 등)
저장 이유:
- 계산 중인 값들
- 스레드 B 가 덮어쓰기 전에 저장
- 복원 시 A 의 계산 이어감
저장/복원:
스레드 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
→ 중단 지점부터 계속
// 자바 스레드의 상태는 OS 가 관리
// 컨텍스트 스위칭은 OS 레벨
Thread t = new Thread(() -> {
int x = 10; // 레지스터/스택
process(x); // PC 이동
// 여기서 스위칭 발생 가능
// x, PC 등 저장됨
int y = x + 5; // 복원 후 계속
});
// 자바 개발자는 직접 컨텍스트 관리 X
// OS 가 처리
// 단, 스레드 수 조절로 스위칭 빈도 영향
// 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"
// 스위칭과 무관하게 같은 스레드면 유지
}
}
저장/복원되는 것은?
답:
1. PC:
레지스터:
스택 포인터:
상태 레지스터:
자바:
컨텍스트 스위칭의 비용:
직접 비용 (Direct Cost):
- 레지스터 저장/복원
- 스케줄러 실행
- 측정 가능 (수 μs)
간접 비용 (Indirect Cost):
- 캐시 미스
- TLB 플러시
- 파이프라인 비우기
- 측정 어려움, 더 클 수 있음
직접 비용:
- 현재 레지스터 저장 (메모리 쓰기)
- 다음 레지스터 복원 (메모리 읽기)
- 스케줄러 알고리즘 실행
- TCB 업데이트
크기:
- 약 1~10 μs (마이크로초)
- 비교적 작고 고정적
간접 비용 (더 큼):
1. 캐시 미스
- 스레드 A 의 데이터가 캐시에
- B 실행 → B 데이터로 캐시 채움
- A 재개 시 → 캐시 미스 (다시 로드)
2. TLB 플러시 (프로세스 전환 시)
- 가상 주소 변환 캐시
- 무효화 → 다시 채움
3. 파이프라인 비우기
- CPU 파이프라인 초기화
크기:
- 직접 비용보다 클 수 있음
- 캐시 미스가 주요 원인
캐시와 컨텍스트 스위칭:
스레드 A 실행:
CPU 캐시: [A 의 데이터로 채워짐]
빠른 접근
컨텍스트 스위칭 → 스레드 B:
CPU 캐시: [B 의 데이터로 교체]
A 데이터 밀려남
A 재개:
CPU 캐시: A 데이터 없음 (캐시 미스)
메인 메모리에서 다시 로드 (느림)
→ 간접 비용
잦은 스위칭의 누적 비용:
작업 시간 10ms, 스위칭 비용 0.1ms:
- 스위칭 10번: 1ms 오버헤드 (10%)
- 스위칭 100번: 10ms 오버헤드 (100%!)
너무 잦으면:
- 실제 작업 < 스위칭 오버헤드
- 처리량 ↓
- 비효율
# Linux 에서 컨텍스트 스위칭 측정
$ vmstat 1
# cs 컬럼 = context switches per second
$ pidstat -w 1
# 프로세스별 컨텍스트 스위칭
# 자바
# JFR (Java Flight Recorder)
# 스레드 상태, 스위칭 분석
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) { }
}
}
직접 비용 vs 간접 비용은?
답:
1. 직접 비용:
간접 비용:
누적:
측정:
CPU 캐시 계층:
레지스터 (가장 빠름, 작음)
↓
L1 캐시 (~1ns, 수십 KB)
↓
L2 캐시 (~4ns, 수백 KB)
↓
L3 캐시 (~10ns, 수 MB, 공유)
↓
메인 메모리 (~100ns, GB)
↓
디스크 (~10ms, TB)
속도 차이:
- 캐시 ↔ 메모리: 약 100배
- 캐시 미스 = 큰 비용
컨텍스트 스위칭 → 캐시 미스:
1. 스레드 A 실행 중
- A 의 데이터가 캐시에
- 빠른 접근
2. 컨텍스트 스위칭 → 스레드 B
- B 의 데이터를 캐시로
- A 데이터 밀려남 (eviction)
3. 스레드 A 재개
- A 데이터가 캐시에 없음
- 캐시 미스
- 메인 메모리에서 다시 (느림)
캐시 지역성 (Locality):
시간 지역성 (Temporal):
- 최근 접근한 데이터 다시 접근
- 캐시에 유지하면 효율
공간 지역성 (Spatial):
- 인접 메모리 함께 접근
- 캐시 라인 단위 로드
컨텍스트 스위칭:
- 지역성 깨뜨림
- 캐시 효율 ↓
// 캐시 친화적 — 스레드 수 최소화
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 등 필요)
}
// 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 방지
}
}
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) { }
}
}
캐시 미스와 컨텍스트 스위칭의 관계는?
답:
1. 캐시 계층:
스위칭 → 캐시 미스:
지역성:
false sharing:
스레드 무한 생성의 문제:
1. 메모리 부족
- 스레드당 ~1MB 스택
- 1만 스레드 = ~10GB
- OutOfMemoryError
2. 컨텍스트 스위칭 폭증
- 코어보다 많으면 잦은 스위칭
- 오버헤드 누적
- 성능 저하
메모리 계산:
스레드 1개:
- 스택 ~1MB (-Xss 기본)
- TCB 등 추가
1,000 스레드:
- 약 1GB 스택
10,000 스레드:
- 약 10GB 스택
- 일반 서버 메모리 초과
- OutOfMemoryError: unable to create new native thread
컨텍스트 스위칭 폭증:
코어 4개, 스레드 1000개:
- 4개만 동시 실행
- 996개 대기
- 끊임없는 스위칭
타임 슬라이스 10ms:
- 1000개 한 바퀴 = 10초
- 각 스레드 매우 가끔 실행
- 스위칭 오버헤드 누적
결과:
- 실제 작업 < 스위칭
- 처리량 급감
스레싱 (Thrashing):
과도한 컨텍스트 스위칭으로
실제 작업보다 전환에 더 많은 시간.
증상:
- CPU 사용률 높지만
- 실제 처리량 낮음
- 시스템 느려짐
원인:
- 너무 많은 스레드/프로세스
- 메모리 부족 (페이지 스와핑)
// ❌ 무한 생성
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();
// 스레드 수 제한 → 스위칭 최소화
}
// ❌ 위험한 패턴
@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();
}
}
스레드를 무한히 만들면 안 되는 이유는?
답:
1. 메모리 부족:
스위칭 폭증:
스레싱:
해결:
코어 4개 시나리오:
스레드 4개 (= 코어 수):
- 4개 모두 동시 실행
- 스위칭 최소
- CPU 바운드에 최적
스레드 100개:
- 4개만 동시
- 96개 대기
- 잦은 스위칭
- CPU 바운드면 비효율 (4개와 비슷하거나 느림)
- I/O 바운드면 효율 (대기 동안 다른 스레드)
CPU 바운드 작업:
이상적 스레드 수 = 코어 수
이유:
- 계산 위주
- 모든 코어 풀가동
- 더 많아도 동시 실행 X
- 스위칭만 늘어남
예:
4코어 → 4 스레드
이미지 처리, 암호화, 계산
I/O 바운드 작업:
이상적 스레드 수 = 코어 수 × (1 + 대기시간/작업시간)
이유:
- I/O 대기 동안 다른 스레드
- 코어보다 많이 필요
예:
4코어, 작업 10ms, I/O 대기 90ms:
4 × (1 + 90/10) = 4 × 10 = 40 스레드
DB 조회, API 호출, 파일
Little's Law:
스레드 수 = 처리율 × 응답 시간
또는:
L = λ × W
- L: 시스템 내 평균 작업 수
- λ: 도착률
- W: 평균 처리 시간
활용:
- 목표 처리율 달성에 필요한 스레드
- 측정 기반 튜닝
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();
// 작업 유형에 따라 분리
}
}
// 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 대기 시 자동 양보
// - 컨텍스트 스위칭 효율
// - 무한 생성 문제 완화
@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();
// 수만 건 동시 조회
}
}
적절한 스레드 수는?
답:
1. CPU 바운드:
I/O 바운드:
Little's Law:
4코어 4 vs 100:
Virtual Thread:
Phase 1 — 동시성의 기초
Unit 1.1 — 멀티태스킹 vs 멀티프로세싱
- 멀티태스킹: 1코어 번갈아 (동시성)
- 멀티프로세싱: 여러 코어 동시 (병렬성)
- 시분할, 스케줄링
Unit 1.2 — 프로세스와 스레드
- 프로세스: 독립 메모리
- 스레드: 실행 흐름
- 공유 (힙) vs 개별 (스택)
Unit 1.3 — 변수 종류와 메모리 위치
- 지역/매개변수: Stack (안전)
- 인스턴스: Heap (조건부)
- static: Method Area (공유)
- "스레드 안전 = 공유되는가?"
Unit 1.4 — 스케줄링 큐와 컨텍스트 스위칭
- 스케줄링 큐
- 컨텍스트 스위칭 (저장/복원)
- 직접/간접 비용
- 적절한 스레드 수
동시성의 기초:
1. 동시 실행의 두 방식
- 멀티태스킹 (동시성)
- 멀티프로세싱 (병렬성)
2. 실행 단위
- 프로세스 (독립)
- 스레드 (공유 + 흐름)
3. 메모리와 안전성
- 공유 = 위험
- 개별 = 안전
4. 비용
- 컨텍스트 스위칭
- 적절한 스레드 수
핵심:
- 공유 메모리 → 동시성 문제
- Phase 4 부터 해결
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: 고급 비동기
Phase 1 의 핵심 통찰 5가지:
1. 동시성 ≠ 병렬성
- 번갈아 vs 진짜 동시
2. 공유가 문제의 근원
- 힙 공유 → 동시성 문제
3. 스레드 안전 = 공유 여부
- 지역 변수는 안전
4. 컨텍스트 스위칭은 비용
- 무한 스레드 X
5. 작업 유형별 스레드 수
- CPU: 코어 수
- I/O: 더 많이
✅ 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%)
Phase 1 의 종합은?
답:
1. 4개 Unit:
큰 그림:
핵심 통찰:
| Q | 핵심 답변 |
|---|---|
| 스케줄링 큐? | 실행 대기 스레드 줄 |
| 컨텍스트 스위칭? | 스레드 전환 (저장/복원) |
| 저장되는 것? | PC, 레지스터, 스택 포인터 |
| 직접 비용? | 레지스터 저장/복원 (μs) |
| 간접 비용? | 캐시 미스, TLB 플러시 |
| 무한 스레드 문제? | 메모리 + 스위칭 폭증 |
| 4코어 4 vs 100? | CPU: 4 최적, I/O: 100 효율 |
| CPU 바운드 스레드 수? | 코어 수 |
| I/O 바운드 스레드 수? | 코어 × (1 + 대기/작업) |
| 스레싱? | 작업 < 스위칭 |
Q1. 멀티태스킹? → 1코어 번갈아
Q2. 멀티프로세싱? → 여러 코어 동시
Q3. 시분할? → CPU 시간 쪼개 할당
Q4. 동시성? → 번갈아 (논리)
Q5. 병렬성? → 동시 (물리)
Q6. 단일 코어 동시 실행? → 멀티태스킹 환상
Q7. 함께 사용? → 가능 (현대 표준)
Q8. 타임 슬라이스? → 작업당 CPU 시간
Q9. 선점형? → OS 강제 전환
Q10. 하이퍼스레딩? → 1코어 2논리
Q11. availableProcessors? → 코어 수
Q12. Rob Pike? → 동시성 dealing, 병렬성 doing
Q13. 프로세스? → 실행 중 프로그램
Q14. 스레드? → 실행 흐름
Q15. 프로그램 vs 프로세스? → 정적 vs 실행 중
Q16. 메모리 4영역? → 코드/데이터/힙/스택
Q17. 공유? → 코드/데이터/힙
Q18. 개별? → 스택
Q19. 스택 개별 이유? → 독립 실행
Q20. 힙 공유 이유? → 협력
Q21. IPC? → 프로세스 간 통신
Q22. PCB? → 프로세스 관리 정보
Q23. TCB? → 스레드 관리 정보
Q24. 멀티프로세스 장점? → 안정성, 격리
Q25. 멀티스레드 장점? → 효율, 통신
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 싱글톤 위험? → 가변 인스턴스 변수
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 효율
50 / 50 → Phase 1 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 1.1 ~ 1.4 재학습
답:
답:
답:
답:
답:
1. 스케줄링 큐 + 컨텍스트 스위칭
2. 비용
3. 적절한 스레드 수
🚀 Phase 1 — 동시성의 기초
✅ Unit 1.1 멀티태스킹 vs 멀티프로세싱
✅ Unit 1.2 프로세스와 스레드
✅ Unit 1.3 변수 종류와 메모리 위치
✅ Unit 1.4 스케줄링 큐와 컨텍스트 스위칭 ← 여기, Phase 1 완주
→ 동시성 vs 병렬성
→ 프로세스/스레드 메모리
→ 스레드 안전 = 공유 여부
→ 컨텍스트 스위칭 비용
Phase 2 — 4분면 매트릭스 (3 Unit)
Unit 2.1 — Sync vs Async (작업 순서 관점)
Unit 2.2 — Blocking vs Non-Blocking (제어권 관점)
Unit 2.3 — 4분면 매트릭스 (★ 마스터, 면접 단골)
✅ Phase 1 — 동시성의 기초 (4 Unit)
⏭ Phase 2 — 4분면 매트릭스 (3 Unit)
총: 4/35 Unit (Phase 1 완주, 약 11%)