F-LAB JAVA · 4주차 · Phase 3 · 스레드 만들고 다루기
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Thread 클래스를 상속하고
run()메서드를 오버라이드한 뒤start()를 호출하면 새 스레드가 생성되어 그 스레드에서run()이 실행된다.
start()는 OS 레벨에서 새 스레드를 생성하고 (NEW → RUNNABLE), 그 새 스레드가run()을 실행한다.
run()을 직접 호출 하면 새 스레드가 생기지 않고 현재 (호출한) 스레드에서 그냥 메서드로 실행 된다 — 멀티스레드가 아니라 일반 메서드 호출.
같은 Thread 인스턴스에start()를 두 번 호출 하면IllegalThreadStateException이 발생한다 (TERMINATED 또는 이미 시작된 스레드는 재시작 불가).
Thread 상속은 간단하지만, 자바는 단일 상속만 가능 하여 다른 클래스를 상속할 수 없고 작업과 스레드 제어가 결합되는 한계가 있다 (다음 Unit 의 Runnable 이 대안).
Thread 상속 = 일꾼 클래스 직접 정의:
class 일꾼 extends Thread {
void run() { 일하기 }
}
start() = 일꾼 출근 (새 일꾼 투입):
- 새 일꾼 (스레드) 고용
- 일꾼이 독립적으로 일 (run)
- 사장은 다른 일
run() 직접 호출 = 사장이 직접 일함:
- 새 일꾼 X
- 사장 (현재 스레드) 이 직접
- 일 끝날 때까지 사장 묶임
start() 두 번 = 이미 퇴근한 일꾼 재고용 시도:
- 불가능 (예외)
- 새 일꾼 고용해야
→ Thread 상속 = 일꾼 정의, start() = 새 일꾼, run() 직접 = 사장이 직접.
1. Thread 클래스 상속 방법
2. run() 오버라이드
3. start()의 동작
4. run() 직접 호출 vs start()
5. 왜 run() 직접 호출은 새 스레드가 없나
6. start() 두 번 호출
7. 상태로 설명 (start vs run)
8. Thread의 주요 메서드와 한계
9. 면접 + 자기 점검
// Thread 상속
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + getName());
}
}
// 사용
MyThread t = new MyThread();
t.start(); // 새 스레드에서 run() 실행
public class Thread implements Runnable {
public Thread() { }
public Thread(String name) { }
public Thread(Runnable target) { }
public void run() { } // 오버라이드 대상
public synchronized void start() { } // 새 스레드 시작
public final String getName() { }
public final void setName(String name) { }
public Thread.State getState() { }
public final boolean isAlive() { }
// ...
}
class WorkerThread extends Thread {
private final String taskName;
public WorkerThread(String taskName) {
super("worker-" + taskName); // 스레드 이름
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("Processing: " + taskName);
}
}
// 사용
WorkerThread t = new WorkerThread("shipment-1");
t.start();
// 익명 클래스
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("Anonymous thread");
}
};
t1.start();
// 람다 (Runnable 전달) — 다음 Unit 정밀
Thread t2 = new Thread(() -> {
System.out.println("Lambda thread");
});
t2.start();
// Thread 상속 (예시 — 실무는 Executor 권장)
public class ShipmentProcessorThread extends Thread {
private final Shipment shipment;
public ShipmentProcessorThread(Shipment shipment) {
super("shipment-processor-" + shipment.getId());
this.shipment = shipment;
}
@Override
public void run() {
log.info("Processing shipment: {}", shipment.getId());
calculateFreight(shipment);
validateShipment(shipment);
log.info("Completed: {}", shipment.getId());
}
private void calculateFreight(Shipment s) { }
private void validateShipment(Shipment s) { }
}
// 사용
ShipmentProcessorThread t = new ShipmentProcessorThread(shipment);
t.start();
Thread 클래스 상속 방법은?
답:
1. 패턴:
생성자:
변형:
실무:
run() 메서드:
스레드가 실행할 작업을 정의.
Thread 상속 시 오버라이드.
기본 구현 (Thread.run):
- Runnable target 이 있으면 실행
- 없으면 아무것도 안 함
// Thread 클래스의 run (기본)
public class Thread {
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run(); // Runnable 있으면 실행
}
// 없으면 빈 메서드
}
}
// 상속 시 오버라이드
class MyThread extends Thread {
@Override
public void run() {
// 작업 정의
}
}
// run() 의 제약
@Override
public void run() { // void 반환, 매개변수 없음
// 작업
}
// 제약:
// - void 반환 (결과 X)
// - 매개변수 없음
// - checked exception 던질 수 없음 (Runnable 의 run)
// 결과나 예외가 필요하면:
// - Callable + Future (Phase 7)
class WorkerThread extends Thread {
@Override
public void run() {
try {
doWork();
} catch (Exception e) {
// run 안에서 처리 (밖으로 전파 X)
log.error("Work failed", e);
}
// checked exception 던질 수 없음
// throws 불가
}
private void doWork() throws Exception {
// ...
}
}
// run 에서 처리 안 한 예외
Thread t = new Thread(() -> {
throw new RuntimeException("Oops"); // 처리 안 함
});
// 핸들러 등록
t.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Uncaught in {}: {}", thread.getName(), throwable.getMessage());
});
t.start();
// 예외 발생 시 핸들러 호출
// 스레드는 TERMINATED
public class ShipmentWorker extends Thread {
private final Shipment shipment;
private final ShipmentService service;
public ShipmentWorker(Shipment shipment, ShipmentService service) {
super("worker-" + shipment.getId());
this.shipment = shipment;
this.service = service;
// 예외 핸들러
setUncaughtExceptionHandler((t, e) ->
log.error("Worker {} failed", t.getName(), e));
}
@Override
public void run() {
try {
service.process(shipment);
} catch (Exception e) {
log.error("Processing failed for {}", shipment.getId(), e);
// checked exception 못 던지니 여기서 처리
}
}
}
run() 오버라이드의 역할은?
답:
1. 역할:
기본 구현:
제약:
예외:
start() 메서드:
새 스레드를 생성하고 시작.
동작:
1. 스레드 상태 확인 (NEW 인지)
2. OS 레벨 스레드 생성
3. 상태 NEW → RUNNABLE
4. 새 스레드가 run() 실행
// start() 의 개념적 동작
public synchronized void start() {
if (threadStatus != 0) { // NEW 아니면
throw new IllegalThreadStateException();
}
// OS 스레드 생성 (네이티브)
start0(); // 네이티브 메서드
// 새 스레드가 run() 호출
}
private native void start0();
public void demonstrateStart() {
Thread t = new MyThread(); // NEW
t.start(); // 새 스레드 생성 + run() 실행
// 현재 스레드: 계속 진행 (이 줄 다음)
// 새 스레드: run() 실행 (병렬)
System.out.println("Main continues"); // 즉시 (run 과 병렬)
}
start() 의 동작:
현재 스레드 (main):
t.start() 호출
↓ (OS 스레드 생성)
━━━━━━━━━━━━━━━━━━ (계속 진행)
새 스레드 (t):
┌──────────────
│ run() 실행 (병렬)
└──────────────
→ 두 스레드 동시 진행
public void asyncNature() {
Thread t = new Thread(() -> {
Thread.sleep(1000); // 1초 작업
System.out.println("Worker done");
});
t.start(); // 즉시 반환 (1초 안 기다림)
System.out.println("After start"); // 바로 출력
// 출력 순서:
// "After start" (즉시)
// "Worker done" (1초 후)
// start() 는 비동기 (run 완료 안 기다림)
}
public class ShipmentParallelProcessor {
public void processInParallel(List<Shipment> shipments) {
List<Thread> workers = new ArrayList<>();
for (Shipment s : shipments) {
Thread worker = new ShipmentWorker(s, service);
worker.start(); // 각각 새 스레드 (병렬 시작)
workers.add(worker);
}
// 모든 워커 병렬 진행
// main 은 계속 (start 비동기)
// 완료 대기 (join — Unit 3.5)
for (Thread worker : workers) {
try {
worker.join(); // 완료까지 대기
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
start()의 동작은?
답:
1. 역할:
동작:
비동기:
흐름:
run() 직접 호출 vs start():
start():
- 새 스레드 생성
- 새 스레드가 run() 실행
- 멀티스레드
run() 직접:
- 새 스레드 X
- 현재 스레드가 run() 실행
- 일반 메서드 호출
class MyThread extends Thread {
@Override
public void run() {
System.out.println("run by: " + Thread.currentThread().getName());
}
}
public void compare() {
MyThread t = new MyThread();
// start() — 새 스레드
t.start();
// 출력: "run by: Thread-0" (새 스레드)
// run() — 현재 스레드
t.run();
// 출력: "run by: main" (현재 스레드!)
}
start():
main 스레드: t.start()
↓ (새 스레드 생성)
새 스레드: run() 실행
main 스레드: 계속 (병렬)
run() 직접:
main 스레드: t.run()
↓
run() 실행 (main 에서!)
↓ (완료까지 main 블록)
다음 코드
public void whichThread() {
Thread t = new Thread(() -> {
System.out.println("Running on: " +
Thread.currentThread().getName());
});
System.out.println("Main: " + Thread.currentThread().getName());
t.start(); // "Running on: Thread-0"
Thread.sleep(100);
t2.run(); // "Running on: main" (run 직접)
}
// ❌ 흔한 실수 — run() 호출
Thread t = new Thread(() -> doWork());
t.run(); // 새 스레드 X, main 에서 실행
// "왜 병렬이 안 되지?" → run() 썼기 때문
// ✓ 올바름 — start()
t.start(); // 새 스레드
public class StartVsRunExample {
public void parallelProcessing(List<Shipment> shipments) {
for (Shipment s : shipments) {
Thread worker = new Thread(() -> process(s));
// ✓ 병렬
worker.start(); // 각각 새 스레드
// ❌ 순차 (실수)
// worker.run(); // main 에서 순차 실행
}
}
private void process(Shipment s) {
log.info("Processing {} on {}",
s.getId(), Thread.currentThread().getName());
}
}
// start(): 여러 스레드 (Thread-0, Thread-1, ...)
// run(): 모두 main 에서 순차
run() 직접 호출 vs start()는?
답:
1. start():
run() 직접:
확인:
실수:
run() 직접 호출 = 일반 메서드 호출:
run() 은 그냥 메서드.
메서드 호출은 현재 스레드에서 실행.
새 스레드 생성은 start() 의 역할.
- start() 가 OS 스레드 생성
- run() 은 작업 정의만
메서드 호출:
obj.method();
- 현재 스레드의 스택에 프레임
- 현재 스레드에서 실행
- 새 스레드 X
run() 도 메서드:
t.run();
- 그냥 메서드 호출
- 현재 스레드 스택
- 현재 스레드에서 실행
start() 가 새 스레드 만드는 이유:
start() 내부:
- start0() 네이티브 호출
- OS 에 새 스레드 요청
- 새 스레드가 run() 호출
run() 직접:
- 네이티브 호출 X
- 그냥 메서드 실행
- 새 스레드 X
start() 내부:
start()
→ start0() (네이티브)
→ OS: 새 스레드 생성
→ 새 스레드가 run() 실행
run() 직접:
run()
→ 메서드 본문 실행 (현재 스레드)
(네이티브 X, OS 스레드 X)
start() — 두 개의 스택:
main 스택:
start() 프레임
(start 후 계속)
새 스레드 스택:
run() 프레임
(독립적)
run() 직접 — 하나의 스택:
main 스택:
run() 프레임 (main 스택에)
(run 완료까지 main 블록)
public class WhyRunNoNewThread {
public void demonstrate() {
Thread t = new Thread(() -> {
String current = Thread.currentThread().getName();
log.info("Running on: {}", current);
// 스택 깊이 확인
log.info("Stack depth: {}",
Thread.currentThread().getStackTrace().length);
});
// start() — 새 스레드 스택
t.start();
// "Running on: Thread-X"
// 독립 스택
Thread.sleep(100);
// run() — main 스택
Thread t2 = new Thread(() ->
log.info("Run on: {}", Thread.currentThread().getName()));
t2.run();
// "Run on: main"
// main 스택에서 실행
}
}
왜 run() 직접 호출은 새 스레드가 없나?
답:
1. 핵심:
메서드 본질:
start() 특별함:
스택:
Thread t = new Thread(() -> doWork());
t.start(); // OK (NEW → RUNNABLE)
t.start(); // ❌ IllegalThreadStateException
// 이미 시작된 스레드 재시작 불가
start() 두 번 안 되는 이유:
스레드는 일회용.
- NEW 에서만 start() 가능
- 한 번 시작하면 NEW 아님
- TERMINATED 후도 NEW 아님
start() 내부:
if (threadStatus != 0) { // NEW 아니면
throw new IllegalThreadStateException();
}
Thread t = new Thread(() -> doWork());
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (NEW 아님)
t.start(); // ❌ NEW 아니므로 예외
// TERMINATED 후도:
t.join();
System.out.println(t.getState()); // TERMINATED
t.start(); // ❌ 여전히 NEW 아니므로 예외
// ❌ 재시작 불가
Thread t = new Thread(task);
t.start();
t.join();
// t.start(); // 예외
// ✓ 새 스레드 생성
Thread t1 = new Thread(task);
t1.start();
t1.join();
Thread t2 = new Thread(task); // 새 인스턴스
t2.start(); // OK
// ✓ 또는 스레드 풀 (재사용)
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(task);
executor.submit(task); // 같은 풀 스레드 재사용
// 스레드 풀 — 스레드 재사용 (Phase 7)
ExecutorService executor = Executors.newFixedThreadPool(2);
// 같은 풀의 스레드가 여러 작업 처리
executor.submit(() -> task1());
executor.submit(() -> task2());
executor.submit(() -> task3());
// 2 스레드가 3 작업 (재사용)
// 풀의 스레드는 작업 후 종료 X
// 다음 작업 대기
// → start() 두 번 문제 회피
public class ThreadReuseExample {
// ❌ 재시작 시도 (실수)
public void badRetry(Shipment shipment) {
Thread worker = new ShipmentWorker(shipment, service);
worker.start();
try {
worker.join();
if (failed) {
worker.start(); // ❌ IllegalThreadStateException
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// ✓ 재시도 — 새 스레드
public void goodRetry(Shipment shipment) throws InterruptedException {
Thread worker = new ShipmentWorker(shipment, service);
worker.start();
worker.join();
if (failed) {
Thread retry = new ShipmentWorker(shipment, service); // 새 인스턴스
retry.start();
}
}
// ✓✓ 스레드 풀 (권장)
private final ExecutorService executor = Executors.newFixedThreadPool(4);
public void poolBased(Shipment shipment) {
executor.submit(() -> service.process(shipment));
executor.submit(() -> service.process(shipment)); // 재사용
}
}
start()를 두 번 호출하면?
답:
1. 결과:
이유:
상태:
해결:
start() 의 상태:
Thread 객체 t (NEW)
↓ t.start()
t (RUNNABLE)
- 새 스레드 생성
- 새 스레드가 run 실행
호출 스레드 (main): 영향 없음 (계속 RUNNABLE)
run() 직접 호출의 상태:
Thread 객체 t (NEW 그대로!)
↓ t.run()
t (NEW 유지)
- run() 은 main 에서 실행
- t 의 상태 변화 X
호출 스레드 (main): run 실행 중 (RUNNABLE)
public void stateComparison() throws InterruptedException {
// start()
Thread t1 = new Thread(() -> {
try { Thread.sleep(100); } catch (Exception e) {}
});
System.out.println("Before start: " + t1.getState()); // NEW
t1.start();
System.out.println("After start: " + t1.getState()); // RUNNABLE
// run()
Thread t2 = new Thread(() -> {
// run 직접
});
System.out.println("Before run: " + t2.getState()); // NEW
t2.run(); // main 에서 실행
System.out.println("After run: " + t2.getState()); // NEW (변화 X!)
// t2 는 시작 안 됨 (run 만 main 에서 호출)
}
start() 상태:
t: NEW → RUNNABLE → ... → TERMINATED
(t 가 실제 스레드로 동작)
run() 직접 상태:
t: NEW → NEW → NEW (변화 없음!)
(t 는 시작 안 함, main 이 run 실행)
main: run 실행 (main 의 작업처럼)
상태로 본 차이:
start():
- t 가 NEW → RUNNABLE
- t 가 실제 스레드
run() 직접:
- t 는 NEW 유지
- run 은 main 에서 (메서드 호출)
- t 는 스레드로 동작 안 함
핵심:
- start: 상태 전이 O
- run: 상태 전이 X
public class StateBasedExplanation {
public void explain() throws InterruptedException {
ShipmentWorker worker = new ShipmentWorker(shipment, service);
// start() — 상태 전이
log.info("Before: {}", worker.getState()); // NEW
worker.start();
log.info("After start: {}", worker.getState()); // RUNNABLE
worker.join();
log.info("After join: {}", worker.getState()); // TERMINATED
// run() — 상태 전이 X (새 인스턴스로)
ShipmentWorker worker2 = new ShipmentWorker(shipment, service);
log.info("Before run: {}", worker2.getState()); // NEW
worker2.run(); // main 에서 실행
log.info("After run: {}", worker2.getState()); // NEW (변화 X!)
}
}
start() vs run()을 상태로 설명하면?
답:
1. start():
run() 직접:
핵심:
Thread t = new Thread(task);
// 생애주기
t.start(); // 시작
t.join(); // 종료 대기
t.interrupt(); // 인터럽트
t.isAlive(); // 살아있는지
// 정보
t.getName(); // 이름
t.setName("name"); // 이름 설정
t.getState(); // 상태
t.getId() / threadId(); // ID
t.getPriority(); // 우선순위
t.setPriority(5); // 우선순위 설정
// 데몬
t.setDaemon(true); // 데몬 설정
t.isDaemon(); // 데몬 여부
// 정적
Thread.currentThread(); // 현재 스레드
Thread.sleep(1000); // 대기
Thread.yield(); // 양보
// 현재 실행 스레드
Thread current = Thread.currentThread();
System.out.println(current.getName()); // 현재 스레드 이름
// 활용
public void logCurrentThread() {
String name = Thread.currentThread().getName();
log.info("Executing on: {}", name);
}
// 현재 스레드 대기 (static)
Thread.sleep(1000); // 1초 (TIMED_WAITING)
// InterruptedException 처리
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 플래그 복원
}
// 주의:
// - 락 반납 X (wait 와 다름)
// - static (현재 스레드)
Thread 상속의 한계:
1. 단일 상속 제약
- 자바는 단일 상속
- Thread 상속 시 다른 클래스 X
2. 작업 + 제어 결합
- 작업 (run) 과 스레드 제어 섞임
- 객체지향적 X
3. 재사용 어려움
- 작업과 스레드 분리 X
- 같은 작업 여러 스레드 어려움
→ Runnable 이 대안 (다음 Unit)
// ❌ Thread 상속 시 다른 클래스 상속 불가
class ShipmentProcessor extends BaseProcessor { // 이미 상속
// extends Thread 불가 (단일 상속)
}
// 해결: Runnable 구현
class ShipmentProcessor extends BaseProcessor
implements Runnable { // 인터페이스는 다중 가능
@Override
public void run() {
// 작업
}
}
new Thread(new ShipmentProcessor()).start();
// Thread 상속 (한계 있음)
public class ShipmentThreadInherit extends Thread {
// BaseService 상속 불가 (이미 Thread)
@Override
public void run() { }
}
// Runnable 구현 (권장 — 다음 Unit)
public class ShipmentRunnable extends BaseService
implements Runnable { // 다른 클래스 + Runnable
@Override
public void run() {
process(); // BaseService 의 메서드 활용
}
}
// 사용
new Thread(new ShipmentRunnable()).start();
// 실무: Executor (Phase 7)
executor.submit(new ShipmentRunnable());
Thread의 주요 메서드와 한계는?
답:
1. 주요 메서드:
한계:
대안:
| Q | 핵심 답변 |
|---|---|
| Thread 상속? | extends Thread + run 오버라이드 |
| run() 역할? | 스레드 작업 정의 |
| start() 동작? | 새 스레드 생성 + run 실행 |
| run() 직접 호출? | 새 스레드 X, 현재 스레드 |
| 왜 새 스레드 없나? | run 은 그냥 메서드 |
| start() 두 번? | IllegalThreadStateException |
| start vs run 상태? | RUNNABLE vs NEW 유지 |
| start() 비동기? | 즉시 반환 (run 대기 X) |
| Thread 한계? | 단일 상속, 결합 |
| run 예외? | checked 못 던짐, 안에서 처리 |
답:
답:
답:
답:
답:
1. Thread 상속
2. start() vs run()
3. 주의와 한계
이번 Unit에서 Thread 상속을 봤다면, 다음은 Runnable 인터페이스 (권장 방식).
🚀 Phase 3 — 스레드 만들고 다루기
✅ Unit 3.1 스레드 상태 다이어그램
✅ Unit 3.2 Thread 클래스 상속 ← 여기
⏭ Unit 3.3 Runnable 인터페이스
⏭ Unit 3.4 데몬 스레드
⏭ Unit 3.5 join()
✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
🚀 Phase 3 — 스레드 다루기 (2/5 진행)
총: 9/35 Unit