runFlag 방식 vs interrupt() 방식기본적인 작업 중단 방식 (runFlag)
volatile boolean runFlag = true;
while (runFlag) {
log("작업 중");
Thread.sleep(3000); // 작업 대기
}
main 스레드에서 runFlag = false로 설정해 중단을 지시합니다.sleep() 중에는 runFlag를 확인할 수 없어 즉시 반응하지 않습니다.문제점: sleep()이 끝날 때까지 대기해야 하므로 반응성이 낮습니다.
runFlag 예제 코드
package thread.control.interrupt;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class ThreadStopMainV1 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(4000);
log("작업 중단 지시 runFlag=false");
task.runFlag = false;
}
static class MyTask implements Runnable {
volatile boolean runFlag = true;
@Override
public void run() {
while (runFlag) {
log("작업 중");
sleep(3000);
}
log("자원 정리");
log("작업 종료");
}
}
}
14:58:27.520 [ work] 작업 중
14:58:30.525 [ work] 작업 중
14:58:31.510 [ main] 작업 중단 지시 runFlag=false
14:58:33.532 [ work] 자원 정리
14:58:33.533 [ work] 작업 종료
main 스레드가 runFlag를 false로 변경해도, work 스레드는 sleep(3000)을 통해 3초간 잠들어 있습니다. 3초간의 잠이 깬 다음에 while(runFlag) 코드를 실행해야, runFlag를 확인하고 작업을 중단할 수 있습니다. 참고로 runFlag를 변경한 후 2초라는 시간이 지난 이후에 작업이 종료되는 이유는 work 스레드가 3초에 한 번씩 깨어나서 runFlag를 확인하는데, main 스레드가 4초에 runFlag를 변경했기 때문입니다. work 스레드 입장에서 보면 두 번째 sleep()에 들어가고 1초 후 main 스레드가 runFlag를 변경합니다. 3초간 sleep()이므로 아직 2초가 더 있어야 깨어납니다.
interrupt()로 작업 중단
thread.interrupt(); // 다른 스레드에 인터럽트 발생
while (true) {
log("작업 중");
Thread.sleep(3000); // 여기서 InterruptedException 발생
}
interrupt() 호출 시 sleep() 상태에서 즉시 InterruptedException 발생합니다.catch (InterruptedException e) {
log("자원 정리");
}
장점: sleep() 상태라도 즉시 반응 가능 → 빠른 종료 처리
인터럽트 상태 직접 확인 (isInterrupted(), Thread.interrupted())
while (!Thread.currentThread().isInterrupted()) {
log("작업 중");
}
isInterrupted()는 상태만 확인합니다. (변경 ❌)Thread.interrupted()는 상태를 확인 + 초기화 (true → false로)자바는 인터럽트 예외가 한 번 발생하면, 스레드의 인터럽트 상태를 다시 정상(false)으로 돌립니다. 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하게 됩니다. 인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 돌려두어야 합니다.
인터럽트의 상태를 직접 체크해서 사용하는 경우 Thread.interrupted()를 사용하면 이런 부분이 해결됩니다. 참고로 isInterrupted()는 특정 스레드의 상태를 변경하지 않고 확인할 때 사용합니다. 물론 꼭 이것만이 정답은 아닙니다. 예를 들어 너무 긴급한 상황이어서 자원 정리도 하지 않고, 최대한 빨리 스레드를 종료
해야 한다면 해당 스레드를 다시 인터럽트 상태로 변경하는 것도 방법입니다.
주의: 인터럽트가 발생해도 처리하지 않으면 이후 sleep() 등에서 또 예외 발생 가능
→ 필요 시 상태를 직접 초기화하거나, 이미 초기화된 상태로 while 탈출
요약 비교
| 방식 | 반응성 | 특징 |
|---|---|---|
runFlag | 느림 | sleep() 중 대기 |
interrupt() | 빠름 | sleep(), wait() 중 즉시 예외 발생 |
isInterrupted() | 확인만 | 상태 그대로 유지 |
Thread.interrupted() | 확인 후 초기화 | 인터럽트 상태 해제됨 |
yield() 실전 적용MyPrinterV1: 기본 구조 (runFlag 방식)
main 스레드: 사용자 입력을 받아 큐에 추가printer 스레드: 큐가 비어있지 않으면 출력 (3초 대기)while (work) {
if (jobQueue.isEmpty()) continue;
String job = jobQueue.poll();
Thread.sleep(3000); // 출력 시간
}
문제점: q를 입력해도 sleep() 중이라 즉시 종료되지 않음
volatile: 여러 스레드가 동시에 접근하는 변수에는 volatile 키워드를 붙어주어야 안전합니다. 여기서는 main 스레드, printer 스레드 둘다 work 변수에 동시에 접근할 수 있습니다. volatile에 대한 자세한 내용은 뒤에서 설명합니다.ConcurrentLinkedQueue : 여러 스레드가 동시에 접근하는 경우, 컬렉션 프레임워크가 제공하는 일반적인 자료구조를 사용하면 안전하지 않습니다. 여러 스레드가 동시에 접근하는 경우 동시성을 지원하는 동시성 컬렉션을 사용해야 합니다. Queue의 경우 ConcurrentLinkedQueue를 사용하면 됩니다. 동시성 컬렉션의 자세한 내용은 뒤에서 설명합니다. 여기서는 일반 큐라고 생각하면 됩니다.MyPrinterV2: 인터럽트 도입
if (input.equals("q")) {
printer.work = false;
printerThread.interrupt(); // 인터럽트 사용
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
break; // 즉시 종료
}
효과: sleep() 중에도 인터럽트로 즉시 종료 가능
➡ 반응성 향상
MyPrinterV3: work 제거, Thread.interrupted() 활용
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) continue;
...
}
interrupted()는 상태 확인 + 초기화work 변수 필요 없음 → 코드 간결해짐MyPrinterV4: yield() 도입
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); // CPU 양보
continue;
}
...
}
이점:
yield vs sleep vs busy-wait
| 방법 | CPU 사용 | 특징 |
|---|---|---|
continue | 높음 | 쉴 틈 없이 while 반복 (busy waiting) |
sleep() | 낮음 | 일정 시간 대기, 응답성 떨어질 수 있음 |
yield() | 낮음 | 잠깐 CPU 양보, 다음 스케줄러에 따라 |
결론: 프린터 예제를 통해 얻은 교훈
interrupt()를 활용하면 즉각적인 종료 제어 가능합니다.Thread.interrupted()를 쓰면 상태 확인 + 초기화로 깔끔하게 종료가 가능합니다.yield()는 불필요한 CPU 낭비를 막고 양보의 힌트를 줄 수 있습니다.yield() 실험: 스레드 양보의 실제 효과실험 개요
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName() + " - " + j);
// ① 아무 것도 안 함
// ② sleep(1)
// ③ Thread.yield()
}
}).start();
}
각 스레드는 0~9까지 출력
실험을 통해 스케줄링 방식에 따라 실행 순서와 효율이 어떻게 달라지는지 비교
① 아무것도 안 한 경우 (Empty)
// 아무 것도 호출하지 않음
특징
효과
② Thread.sleep(1)
Thread.sleep(1);
특징
효과
③ Thread.yield()
Thread.yield();
특징
효과
참고로 최근에는 10코어 이상의 CPU도 많기 때문에 스레드 10개 정도만 만들어서 실행하면, 양보가 크게 의미가 없습니다. 양보해도 CPU 코어가 남기 때문에 양보하지 않고 계속 수행될 수 있습니다. CPU 코어 수 이상의 스레드를 만들어야 양보하는 상황을 확인할 수 있습니다. 그래서 이번 예제에서 1000개의 스레드를 실행한 것 입니다.
참고: log()가 사용하는 기능은 현재 시간도 획득해야 하고, 날짜 포멧도 지정해야 하는 등 복잡합니다. 이 사이에 스레드의 컨텍스트 스위칭이 발생하기 쉽습니다. 이런 이유로 스레드의 실행 순서를 일정하게 출력하기 어렵습니다. 그래서 여기서는 단순한 System.out.println()을 사용했습니다.
yield vs sleep 요약 비교
| 항목 | yield() | sleep() |
|---|---|---|
| 상태 변화 | RUNNABLE 유지 | TIMED_WAITING으로 진입 |
| 양보 확실성 | 보장 안됨 (힌트) | 시간만큼 확실히 양보 |
| 성능 영향 | 적음 | 약간 있음 (대기 시간 발생) |
| 언제 쓰나? | 부하 적고 민감할 때 | 정확한 시간 제어 필요할 때 |
실전 팁
yield()는 의미가 없다 → 본인이 계속 실행됨yield()의 효과가 드러남yield() 또는 sleep()은 오히려 성능 저하 유발 가능Thread.yield() 정리
개념
Thread.yield()는 현재 실행 중인 스레드가 CPU 사용을 자발적으로 양보하는 메서드입니다.RUNNABLE 상태를 유지한 채 스케줄링 큐의 뒤로 물러나며, 다른 스레드가 실행될 기회를 얻게 합니다.작동 방식
Thread.yield();
특징 요약
| 항목 | 설명 |
|---|---|
| 상태 | 여전히 RUNNABLE 상태 유지 |
| 스케줄링 | 스케줄러에게 힌트 제공 (필수 아님) |
| 목적 | 다른 스레드에게 실행 기회 양보 |
| 반환 | void, 실행 결과를 확인할 수 없음 |
| 효과 | CPU 자원을 덜 소모할 수 있음 (busy loop 방지 등) |
언제 사용하나?
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); // CPU 양보
continue;
}
...
}
주의사항
yield()는 별 효과 없음