4주차 자료의 모든 토픽을 "기초 → 동기화 → 협력 → 추상화" 순서로 재배열한 학습 경로.
1~3주차에서 단일 스레드 흐름을 봤다면, 4주차는 여러 스레드가 동시에 움직이는 세계 의 모든 것을 다룬다.
면접·실무에서 가장 자주 등장하는 영역이며, 분량도 가장 많다.
[Phase 1] 동시성의 기초 — 프로세스와 스레드
↓
[Phase 2] Sync/Async × Blocking/Non-Blocking 4분면
↓
[Phase 3] 스레드 만들고 다루기
↓
[Phase 4] 동기화 — synchronized와 메모리 가시성 ◄── 4주차 1차 정점
↓
[Phase 5] 정교한 락 — LockSupport와 ReentrantLock
↓
[Phase 6] 스레드 간 협력 — 생산자/소비자, 인터럽트, yield
↓
[Phase 7] 직접 사용의 한계와 Executor 프레임워크 ◄── 4주차 2차 정점
↓
[Phase 8] 고급 비동기 — CompletableFuture와 ForkJoinPool
총 8 Phase × 35 Unit — 실습 비중이 매우 높아 압축 7일 또는 여유 14일 권장.
| 영역 | 1주차 | 2주차 | 3주차 | 4주차 (지금) |
|---|---|---|---|---|
| 메모리 모델 | Heap/Stack | Method Area 3분할 | (없음) | 스레드별 캐시 vs 메인 메모리, volatile |
| 컬렉션 | 개요 | 내부 구조 | 전체 지도 | ConcurrentHashMap, ConcurrentLinkedQueue |
| I/O | 개요 | (없음) | NIO/Stream/Channel | Blocking vs Non-Blocking 깊이 |
| 신규 | OOP | Reflection | 람다/스트림 | synchronized, Lock, Executor, CompletableFuture, ForkJoin |
| Day | Phase | 학습 목표 |
|---|---|---|
| 1일차 | Phase 1 + 2 | 프로세스/스레드 기초 + 4분면 매트릭스 |
| 2일차 | Phase 3 | Thread/Runnable, 데몬, join() |
| 3일차 | Phase 4 | synchronized + volatile (★ 1차 정점) |
| 4일차 | Phase 5 | LockSupport, ReentrantLock, tryLock |
| 5일차 | Phase 6 | wait/notify, 인터럽트, yield |
| 6일차 | Phase 7 | Executor 프레임워크 전체 (★ 2차 정점) |
| 7일차 | Phase 8 | CompletableFuture, ForkJoinPool |
여유 일정 (14일): Phase 4·7은 각 2일씩 배정. 동시성은 코드를 직접 돌려봐야 체화됨.
목표: 멀티태스킹·멀티프로세싱·프로세스·스레드의 정의와 메모리 구조를 명확히 잡는다.
선수 지식: 1주차 Phase 4 (JVM)
핵심 개념
| 구분 | 멀티태스킹 | 멀티프로세싱 |
|---|---|---|
| 정의 | 1개 CPU(코어)가 여러 작업을 번갈아 실행 | 여러 CPU(코어)가 동시에 작업 |
| 기반 | 소프트웨어 (시분할 스케줄링) | 하드웨어 (다중 코어) |
| 예시 | OS의 멀티프로그램 환경 | 현대 멀티코어 CPU |
자기 점검
선수 지식: Unit 1.1
핵심 개념
프로세스:
스레드:
메모리 구성:
프로세스 메모리
├── 코드 섹션 ┐
├── 데이터 섹션 │ 모든 스레드가 공유
├── 힙(Heap) ┘
└── 스택(Stack) ← 스레드마다 개별 할당
자기 점검
선수 지식: Unit 1.2, 2주차 Phase 1
핵심 개념
멀티스레드 관점에서의 변수 매핑:
| 변수 | 저장 위치 | 멀티스레드 관점 |
|---|---|---|
| 지역 변수 | Stack (스레드별) | 스레드 안전 (공유 안 됨) |
| 인스턴스 변수 | Heap | 공유 → 동기화 필요 |
| 클래스 변수 (static) | Method Area (Data) | 공유 → 동기화 필요 |
| 전역 변수 (static) | Method Area (Data) | 공유 → 동기화 필요 |
핵심 통찰: "스레드 안전한가?"의 답은 대부분 "공유되는가?" 와 같다.
자기 점검
선수 지식: Unit 1.2
핵심 개념
스케줄링 큐:
컨텍스트 스위칭(Context Switching):
자기 점검
목표: 4가지 용어가 면접에서 자주 헷갈리는데, "무엇이 다른 축인가" 를 명확히 잡는다.
선수 지식: Phase 1
핵심 개념
핵심 질문: "다음 작업을 위해 이전 작업의 완료 여부를 확인 하는가?"
자기 점검
선수 지식: Unit 2.1
핵심 개념
핵심 질문: "OS(또는 호출된 함수)가 제어권을 가져갔는가, 즉시 돌려줬는가?"
제어권의 의미:
자기 점검
선수 지식: Unit 2.1, Unit 2.2
핵심 개념
| Blocking | Non-Blocking | |
|---|---|---|
| Sync | 가장 단순 (전통 IO) | Polling 방식 |
| Async | Future.get() | CompletableFuture, Callback |
4가지 조합 정리:
Socket.read())socketChannel.read() 가 0 반환 시 다른 일Future.get()CompletableFuture자기 점검
목표: 스레드를 직접 만들고 상태를 추적하며 제어한다.
선수 지식: Phase 1
핵심 개념
NEW ──start()──> RUNNABLE ──작업종료──> TERMINATED
│ ↑↓ (CPU 스케줄러)
│
┌─────────┼─────────────┐
↓ ↓ ↓
BLOCKED WAITING TIMED_WAITING
(synchronized) (wait, join) (sleep, parkNanos)
상태 전이:
NEW → RUNNABLE: start() 호출RUNNABLE → BLOCKED: synchronized 락 대기RUNNABLE → WAITING: wait(), join()RUNNABLE → TIMED_WAITING: sleep(ms), wait(ms)* → TERMINATED: run 종료자기 점검
getState()는 RUNNING을 반환할 수 있는가?선수 지식: Unit 3.1
핵심 개념
Thread 클래스 상속 → run() 오버라이드start() 호출로 실제 스레드 시작run()을 직접 호출하면 새 스레드가 생기지 않음 (그냥 메서드 호출)class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
new MyThread().start(); // 새 스레드에서 실행 ✅
new MyThread().run(); // 현재 스레드에서 실행 ❌
자기 점검
run() 직접 호출과 start() 호출의 차이를 스레드 상태로 설명하라start()를 두 번 호출하면?선수 지식: Unit 3.2
핵심 개념
Thread 상속의 한계:
Runnable의 장점 3가지:
1. 다른 클래스 상속 가능 (자바 단일 상속 제약 회피)
2. 작업과 스레드 분리 (객체지향적 설계)
3. 메모리 효율 (여러 스레드가 같은 Runnable 공유 가능)
Runnable task = () -> System.out.println("Task");
new Thread(task).start();
new Thread(task).start(); // 같은 작업을 2개 스레드가 실행
자기 점검
@Async는 내부적으로 Thread vs Runnable 중 무엇을 쓸까?선수 지식: Unit 3.3
핵심 개념
setDaemon(true) 로 설정 (start() 전에)용도: GC 스레드, 모니터링, 로깅 등 백그라운드 보조 작업
주의: 작업 완료 보장이 필요하면 데몬으로 설정 금지
자기 점검
선수 지식: Unit 3.4
핵심 개념
t.join(): t 스레드가 종료될 때까지 현재 스레드를 대기병렬 실행 vs 직렬 실행:
// 병렬 (3초)
t1.start(); t2.start();
t1.join(); t2.join();
// 직렬 (5초) ← 잘못된 패턴
t1.start(); t1.join();
t2.start(); t2.join();
자기 점검
join() 호출 시 호출자 스레드의 상태는?목표: 멀티스레드의 가장 큰 적인 데이터 불일치 를 막는 가장 기본 도구를 마스터한다.
선수 지식: Phase 3
핵심 개념
고전적 예제:
class Counter {
int count = 0;
void increment() { count++; } // ← 임계 영역
}
count++ 는 사실 읽기 → 1 더하기 → 쓰기 3단계 → 두 스레드가 동시 실행 시 결과 손실
자기 점검
count++ 의 어셈블리 수준 동작을 설명해보라선수 지식: Unit 4.1
핵심 개념
synchronized 키워드this (해당 인스턴스)class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
자기 점검
Class 객체)선수 지식: Unit 4.2
핵심 개념
public void increment() {
// 비동기 영역
synchronized (this) { // ← 임계 영역만 잠금
count++;
}
// 비동기 영역
}
선택 가이드:
synchronized 메서드synchronized 블록 (성능 ↑)자기 점검
synchronized (this) 와 synchronized (new Object()) 의 차이는?선수 지식: Unit 4.3
핵심 개념
락 경쟁 시나리오:
synchronized의 단점:
자기 점검
선수 지식: Unit 4.4
핵심 개념
문제 시나리오:
boolean runFlag = true;
// 스레드 A: while (runFlag) { ... }
// 스레드 B: runFlag = false;
// → A가 영원히 종료 안 됨!
원인: 각 CPU 코어는 자기 캐시 메모리 를 사용
false 변경이 메인 메모리에 반영 안 됨true해결: volatile:
volatile boolean runFlag = true;
중요: synchronized 안의 변수는 자동으로 가시성 보장됨
자기 점검
volatile이 원자성도 보장하는가? (힌트: NO. count++는 여전히 위험)volatile과 synchronized의 차이를 한 문장으로?목표: synchronized의 한계를 ReentrantLock으로 극복하고, 데드락을 회피하는 패턴을 익힌다.
선수 지식: Phase 4
핵심 한계 3가지:
1. 무한 대기: 타임아웃 설정 불가
2. 인터럽트 불가: 대기 중 외부에서 깨울 수 없음
3. 공정성 X: 어떤 스레드가 락을 받을지 보장 없음
해결책: 자바는 더 정교한 락 도구를 제공
자기 점검
선수 지식: Unit 5.1
핵심 개념
LockSupport의 3가지 메서드:
park(): 현재 스레드를 WAITING 상태로 (주차)parkNanos(nanos): TIMED_WAITING 상태로 (시간 한정)unpark(thread): 대상 스레드를 RUNNABLE로 복귀Thread t = new Thread(() -> {
LockSupport.park();
System.out.println("깨어남");
});
t.start();
Thread.sleep(100);
LockSupport.unpark(t); // 깨움
특징:
자기 점검
선수 지식: Unit 5.2
핵심 개념
Lock 인터페이스의 구현체 (java.util.concurrent.locks)try-finally 필수 (락 반납 보장)private final Lock lock = new ReentrantLock();
public boolean withdraw(int amount) {
lock.lock(); // 락 획득
try {
// 임계 영역
balance -= amount;
return true;
} finally {
lock.unlock(); // 반드시 finally에서!
}
}
왜 finally가 필수인가:
자기 점검
lock() 과 synchronized 의 차이를 한 문장으로?unlock() 을 깜박하면 어떤 사고가?선수 지식: Unit 5.3
핵심 개념
tryLock()의 두 형태:
boolean tryLock(): 즉시 시도, 실패 시 falseboolean tryLock(time, unit): 지정 시간만큼 시도if (!lock.tryLock()) {
log("이미 처리중인 작업이 있습니다.");
return false; // 무한 대기 안 함
}
try {
// 임계 영역
} finally {
lock.unlock();
}
데드락 회피 시나리오:
lock() 이면 영원히 멈춤tryLock() 이면 한 쪽이 포기하고 재시도 → 데드락 회피자기 점검
lock() 만 쓰는 시스템에서 데드락이 발생하면 어떻게 회복하는가?tryLock(5, SECONDS) 가 실패한 후 어떤 전략을 쓸 수 있나?목표: 스레드들이 단순히 경쟁하는 것이 아니라 협력 하는 패턴을 익힌다.
선수 지식: Phase 4
핵심 개념
문제 정의:
실제 사례: 메시지 큐(MQ), 로깅 시스템, 작업 큐
자기 점검
선수 지식: Unit 6.1
핵심 개념
wait(): synchronized 안에서 호출 → 락을 반납하고 WAITING 상태로notify(): WAITING 중인 스레드 1개를 깨움notifyAll(): 모든 WAITING 스레드를 깨움synchronized (lock) {
while (queue.isEmpty()) {
lock.wait(); // 락 반납 + 대기
}
// 데이터 처리
lock.notify(); // 다른 스레드 깨움
}
왜 if가 아니라 while?:
자기 점검
wait()과 sleep()의 결정적 차이는? (힌트: 락)notify()와 notifyAll() 중 어느 것을 권장? (힌트: 안전성)선수 지식: Phase 3
핵심 개념
3가지 메서드 구분:
| 메서드 | 동작 | 플래그 변화 |
|---|---|---|
interrupt() | 인터럽트 신호 발송 | true로 설정 |
isInterrupted() | 플래그 확인 (변경 X) | 변화 없음 |
interrupted() (static) | 플래그 확인 후 false로 | true → false |
블로킹 메서드의 동작:
sleep(), wait(), join() 중에 인터럽트 받으면InterruptedException 발생즉각 종료 패턴:
while (!Thread.interrupted()) { // 매번 플래그 체크
// 일 수행
}
자기 점검
interrupt() 만 호출했는데 스레드가 즉시 종료 안 되는 이유는?sleep() 도중 인터럽트가 들어오면 어떻게 되는가?선수 지식: Unit 6.3
핵심 개념
yield(): "다른 스레드에 CPU를 양보하겠다"는 힌트용도: 바쁜 대기(Busy Waiting) 상황에서 CPU 자원 절약
while (!Thread.interrupted()) {
if (queue.isEmpty()) {
Thread.yield(); // CPU 양보
continue;
}
// 작업
}
상태 비교:
| 메서드 | 결과 상태 |
|---|---|
| sleep() | RUNNABLE → TIMED_WAITING → RUNNABLE |
| yield() | RUNNABLE → RUNNABLE (잠깐 양보) |
| wait() | RUNNABLE → WAITING → RUNNABLE |
자기 점검
yield() 가 OS의 보장된 동작인가? (힌트: 단순 힌트)목표: 스레드 직접 사용의 모든 한계를 한 번에 해결하는 자바 표준 동시성 도구를 마스터한다.
선수 지식: Phase 3 ~ 6
핵심 문제:
생성 비용:
관리 문제:
Runnable의 불편함:
void run())해결책 = 스레드 풀 + 반환값 가능한 작업 인터페이스 = Executor 프레임워크
자기 점검
선수 지식: Unit 7.1
핵심 개념
3개 타입의 역할 분담:
| 이름 | 역할 |
|---|---|
Executor (인터페이스) | 작업 실행만 (execute(Runnable)) |
ExecutorService (인터페이스) | + 작업 제어 (submit, shutdown, invokeAll) |
Executors (유틸 클래스) | ExecutorService 팩토리 |
관계:
Executor (실행만)
└─ ExecutorService (실행 + 제어)
↑ 생성
└─ Executors.newFixedThreadPool() 등
자기 점검
Executor 와 ExecutorService 를 분리한 이유는? (힌트: ISP)new ThreadPoolExecutor(...) 대신 Executors.xxx() 를 쓰나?선수 지식: Unit 7.2
핵심 개념
| 팩토리 메서드 | 특징 | 적합 상황 |
|---|---|---|
newFixedThreadPool(n) | 고정 n개 스레드 | 일반 서버, 안정성 우선 |
newCachedThreadPool() | 필요 시 무한 생성, 60초 후 회수 | 짧은 작업 다수 |
newSingleThreadExecutor() | 1개 스레드 | 순서 보장 필요 |
newScheduledThreadPool(n) | 주기적/지연 실행 | 스케줄러, 배치 |
중요 주의:
newCachedThreadPool() 은 트래픽 폭주 시 위험 (스레드 폭증)자기 점검
선수 지식: Unit 7.3
핵심 개념
Runnable vs Callable:
| 항목 | Runnable | Callable |
|---|---|---|
| 메서드 | void run() | V call() throws Exception |
| 반환값 | ❌ | ✅ (제네릭) |
| 체크 예외 | ❌ | ✅ |
Future:
submit() 의 반환 타입get() 으로 결과 회수 (블로킹)ExecutorService es = Executors.newFixedThreadPool(2);
Future<Integer> future = es.submit(() -> {
Thread.sleep(2000);
return 42;
});
log("future 즉시 반환 (작업은 백그라운드에서 진행)");
Integer result = future.get(); // 여기서 블로킹
자기 점검
submit() 호출 즉시 Future가 반환되는데, 작업은 어디서 실행되는가?future.get() 호출 전에 작업이 이미 끝났다면?선수 지식: Unit 7.4
핵심 개념
cancel(mayInterruptIfRunning):
true: 실행 중인 작업에 인터럽트 발생 → 즉시 중단 시도false: 실행 중이면 그냥 둠, 새로 실행은 안 함이후 get() 호출 시 → CancellationException
자기 점검
Thread.sleep(1000) 중인데 cancel(true) 호출 시 어떻게 되는가?cancel(true) 가 효과 있는가?선수 지식: Unit 7.5
핵심 개념
| 메서드 | 동작 |
|---|---|
invokeAll(tasks) | 모든 작업 완료 까지 대기, 모든 결과 반환 |
invokeAny(tasks) | 첫 번째 완료된 작업 의 결과 반환, 나머지 취소 |
활용 시나리오:
invokeAll: 여러 외부 API 호출 후 모든 결과 종합invokeAny: 여러 후보 중 가장 빨리 응답한 것 채택 (예: 미러 서버)자기 점검
invokeAll 도중 한 작업이 예외를 던지면?invokeAny 가 가장 빠른 응답을 어떻게 결정하는가?선수 지식: Unit 7.6
핵심 개념
| 메서드 | 새 작업 거절 | 진행 중 작업 | 큐의 작업 |
|---|---|---|---|
shutdown() | ✅ | 마무리 | 마무리 |
shutdownNow() | ✅ | 인터럽트 | 포기 (반환됨) |
안전 종료 패턴:
es.shutdown();
if (!es.awaitTermination(60, SECONDS)) {
es.shutdownNow(); // 60초 내 안 끝나면 강제
}
자기 점검
shutdownNow() 가 반환하는 List에는 무엇이 담기는가?목표: Future의 한계(블로킹 회수)를 넘는 진정한 비동기 + 병렬 처리 도구를 익힌다.
선수 지식: Unit 7.4
Future의 한계:
future.get())CompletableFuture의 해결:
thenApply, thenAccept, thenCombine 등 체이닝CompletableFuture.supplyAsync(() -> 5)
.thenApply(x -> x * 2) // 변환
.thenCombine(otherFuture, Integer::sum) // 결합
.thenAccept(System.out::println); // 소비
System.out.println("Main thread continues..."); // 즉시 실행
자기 점검
Future.get() 과 CompletableFuture.thenAccept() 의 본질적 차이는?선수 지식: Phase 7
핵심 개념
일반 스레드 풀의 한계:
ForkJoinPool의 아이디어:
효과: 모든 스레드가 쉴 틈 없이 일함 → CPU 사용률 극대화
자기 점검
선수 지식: Unit 8.2
핵심 개념
| 클래스 | 반환값 |
|---|---|
RecursiveTask<V> | 있음 (V) |
RecursiveAction | 없음 (void) |
구현 패턴:
1. compute() 오버라이드
2. 작업이 작으면 직접 처리
3. 크면 둘로 나눠서:
leftTask.fork() (비동기 시작)rightResult = rightTask.compute() (동기 처리)leftResult = leftTask.join() (대기 후 합산)class SumTask extends RecursiveTask<Long> {
protected Long compute() {
if (작업이 작으면) return 직접계산();
SumTask left = new SumTask(start, mid);
SumTask right = new SumTask(mid+1, end);
left.fork();
long r = right.compute();
long l = left.join();
return l + r;
}
}
자기 점검
fork() 와 compute() 를 동시에 호출하지 않고 나눠 쓰는 이유는?parallelStream() 은 내부적으로 무엇을 사용하는가?start() 와 run() 의 차이는?lock() 다음에 반드시 try-finally 가 필요한 이유는?tryLock() 이 데드락을 어떻게 방지하는가?wait() 가 synchronized 안에서만 동작하는 이유는?interrupt(), interrupted(), isInterrupted() 의 차이는?yield() 와 sleep() 의 상태 차이는?newCachedThreadPool() 의 위험성은?Future.get() 과 CompletableFuture.thenAccept() 의 차이는?shutdown() 과 shutdownNow() 중 안전 종료에 적합한 것은?반드시 깊이 파기 (면접·실무 직결):
[ ] Phase 1 — 동시성 기초 (Unit 1.1~1.4)
[ ] Phase 2 — 4분면 매트릭스 (Unit 2.1~2.3)
[ ] Phase 3 — 스레드 다루기 (Unit 3.1~3.5)
[ ] Phase 4 — synchronized & volatile (Unit 4.1~4.5) ★ 1차 정점
[ ] Phase 5 — Lock 도구 (Unit 5.1~5.4)
[ ] Phase 6 — 스레드 협력 (Unit 6.1~6.4)
[ ] Phase 7 — Executor 프레임워크 (Unit 7.1~7.7) ★ 2차 정점
[ ] Phase 8 — 고급 비동기 (Unit 8.1~8.3)
[ ] 종합 자기 점검 31문항 통과
필수 실습 코드:
1. Race condition 직접 재현 (synchronized 없는 카운터 증가)
2. 데드락 시나리오 (락 2개 잡는 두 스레드)
3. wait/notify로 생산자-소비자 구현
4. ExecutorService로 같은 작업을 풀로 처리
5. CompletableFuture 체이닝 (3개 API 호출 합치기)
디버깅 도구:
jstack <pid>: 스레드 덤프 (BLOCKED/WAITING 확인)-XX:+PrintGCDetails: GC와 스레드의 상호작용