1. 인터럽트란?

특정 스레드가 WAITING, TIMED_WAITING 같은 대기 상태일 때 강제로 깨워 RUNNABLE 상태로 전환시키는 메커니즘
- thread.interrupt()를 호출하면 해당 스레드의 인터럽트 상태(flag)가 true로 변경됨
- sleep(), wait() 등 InterruptedException을 던지는 메서드 실행 중이라면 즉시 예외 발생
- while(true), 일반 연산 중에는 예외가 발생하지 않음 (인터럽트 상태만 true로 남음)
2. runFlag 방식의 한계
가장 단순한 스레드 중단 방법은 volatile 변수를 사용하는 것이다.
while (runFlag) {
log("작업 중");
sleep(3000); // 3초 대기
}
// main이 4초 뒤에 runFlag = false 변경
14:58:31.510 [ main] 작업 중단 지시 runFlag=false
14:58:33.532 [ work] 자원 정리 // 2초 후에야 반응
문제: sleep(3000) 중에는 runFlag를 확인하지 못한다. 최악의 경우 변경 후 3초 뒤에야 반응한다.
3. interrupt() 도입
interrupt()를 사용하면 sleep() 중인 스레드를 즉시 깨울 수 있다.
// main 스레드
thread.interrupt();
// work 스레드
try {
while (true) {
log("작업 중");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
// sleep() 중 인터럽트 발생 → 즉시 catch로 이동
log("인터럽트 발생, 작업 종료");
}
18:10:44.011 [ main] 작업 중단 지시 thread.interrupt()
18:10:44.021 [ work] 인터럽트 발생, 작업 종료 // 즉시 반응
인터럽트 예외 발생 시 상태 변화
- 예외 발생 전: 인터럽트 상태 = true
- 예외 발생 후: 인터럽트 상태 = false (자동 초기화)
InterruptedException이 발생하면 자바가 자동으로 인터럽트 상태를 false로 되돌린다. 이후 코드(자원 정리 등)에서 의도치 않은 인터럽트가 재발생하지 않도록 하기 위함이다.
4. isInterrupted() vs Thread.interrupted()
while 조건에서 인터럽트 상태를 직접 체크할 때는 두 메서드의 차이를 반드시 알아야 한다.
메서드 반환값 인터럽트 상태 변경 용도
| isInterrupted() | true/false | 변경 안 함 | 단순 상태 확인 |
|---|---|---|---|
| Thread.interrupted() | true/false | true이면 false로 초기화 | while 조건 직접 체크 시 사용 권장 |
isInterrupted() 사용 시 문제
while (!Thread.currentThread().isInterrupted()) {
// 루프 탈출 후에도 인터럽트 상태가 true로 유지
}
log("자원 정리 시도");
Thread.sleep(1000); // 인터럽트 상태가 true라서 즉시 예외 발생!
Thread.interrupted() 사용 시 해결
while (!Thread.interrupted()) {
// 루프 탈출 시점에 인터럽트 상태를 false로 초기화
}
log("자원 정리 시도");
Thread.sleep(1000); // 정상 동작
log("자원 정리 완료");
인터럽트의 목적을 달성한 뒤에는 인터럽트 상태를 반드시 정상(false)으로 되돌려야 한다. while 조건에서 직접 체크할 때는 Thread.interrupted()를 사용하자.
5. 프린터 예제로 보는 인터럽트 활용
사용자 입력을 출력하는 프린터 스레드를 q 입력으로 즉시 종료하는 예제다.
V1 (runFlag만 사용) - 문제
if (input.equals("q")) {
printer.work = false; // sleep 중이면 최대 3초 뒤 반응
}
V2 (interrupt 추가) - 개선
if (input.equals("q")) {
printer.work = false;
printerThread.interrupt(); // sleep 즉시 깨움
}
- work=false: while 조건에서 빠져나옴
- interrupt(): sleep() 중인 상태에서 즉시 빠져나옴
- 두 가지를 함께 쓰면 어떤 상황에서도 즉시 종료 가능
V3 (Thread.interrupted()로 work 변수 제거) - 최종
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); // 대기 중에는 CPU 양보
continue;
}
// 출력 처리
}
6. yield - CPU 양보
Thread.yield()는 현재 스레드가 자발적으로 CPU를 반납하고 스케줄링 큐 맨 뒤로 돌아가는 힌트를 OS에 제공한다.
방식 상태 변화 특징
| 아무것도 안 함 | RUNNABLE 유지 | 한 스레드가 쭉 실행되기도 함. OS에 완전 위임 |
|---|---|---|
| sleep(1) | RUNNABLE → TIMED_WAITING → RUNNABLE | 강제 1ms 대기. 양보할 스레드 없어도 쉼 (비효율) |
| Thread.yield() | RUNNABLE 유지 (스케줄링 큐 재진입) | 양보할 스레드 없으면 자신이 계속 실행. 효율적 |
- yield()는 강제성이 없다. OS 스케줄러에 힌트를 줄 뿐이며, 반드시 다른 스레드가 실행되는 것은 아니다.
- CPU 코어 수보다 스레드가 많을 때 의미가 있다. 코어 수와 비슷한 스레드 수라면 양보 효과가 거의 없다.
7. 메모리 가시성 문제란?
멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지 보장이 안 되는 문제
boolean runFlag = true;
public void run() {
while (runFlag) { // runFlag = false가 되길 기다림
}
log("task 종료");
}
// main 스레드: 1초 후 runFlag를 false로 변경
task.runFlag = false;
기대 결과: main이 runFlag = false로 바꾸면 work 스레드가 즉시 종료
실제 결과: work 스레드가 종료되지 않고 무한 루프 지속
분명히 runFlag = false가 콘솔에 찍혀는데, work 스레드는 여전히 true로 인식한다!
8. 캐시 메모리와 메인 메모리
일반적으로 생각하는 메모리 접근 방식
- main 스레드가 runFlag = false로 변경 → 메인 메모리에 즉시 반영
- work 스레드가 메인 메모리에서 runFlag를 읽으면 false 확인 → while 탈출
- 단순하고 직관적이지만, 이렇게 동작하지 않는다!
실제 메모리 접근 방식 (캐시 메모리)


계층 특징 속도
| CPU 레지스터 | 가장 빠름, 극소 용량 | 매우 빠름 |
|---|---|---|
| 캐시 메모리 | 코어별 보유, 작은 용량, 비쎀 | 빠름 |
| 메인 메모리 | 크고 저렴, CPU와 거리가 멀 | 상대적으로 느림 |
캐시 메모리가 문제가 되는 이유
- main 스레드가 runFlag = false로 변경 → 코어1의 캐시 메모리에만 반영
- 메인 메모리에 반영되는 시점: 알 수 없다 (CPU 설계에 따라 다름)
- work 스레드의 캐시 메모리에 반영되는 시점: 마찬가지로 알 수 없다
- 극단적으로는 평생 반영되지 않을 수도 있다!
메모리 가시성(Memory Visibility): 멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제
캐시 갱신이 일어나는 경우 (보장은 아님): 컨텍스트 스위칭 발생 시 주로 갱신, Thread.sleep() 호출 시, 콘솔 출력시
9. volatile 키워드
해결 방법: volatile
값을 읽거나 쓸 때 캐시 메모리를 거치지 않고 항상 메인 메모리에 직접 접근
// volatile 미적용 (문제 발생)
boolean runFlag = true;
// volatile 적용 (메모리 가시성 보장)
volatile boolean runFlag = true;

volatile 성능 트레이드오프
구분 volatile 미적용 volatile 적용
| 카운트 (1초 기준) | 약 11억 회 | 약 2.2억 회 |
|---|---|---|
| 성능 | 빠름 (캐시 활용) | 약 5배 느림 |
| 가시성 보장 | X 보장 안 됨 | O 즉시 보장 |
성능을 약간 희생하는 대신 스레드 간 가시성을 보장한다. 꼭 필요한 곳에만 사용하자.
- 참고: volatile과 내부 최적화
- volatile은 단순히 "메인 메모리에 직접 접근"으로 설명하지만, 실제로는 **메모리 배리어(Memory Barrier)**를 삽입해 JIT 컴파일러의 재정렬을 막고 가시성을 보장한다. 더 깊이 알고 싶다면 '메모리 배리어', 'JIT 컴파일러 volatile 최적화' 키워드로 검색해보자.
10. Java Memory Model (JMM)
자바 프로그램이 어떻게 메모리에 접근하고 수정할 수 있는지를 규정하는 명세. 특히 멀티스레드 환경에서 스레드 간 상호작용을 정의한다.
- JMM의 핵심 = happens-before 관계
11. happens-before 관계
개념
A 작업이 B 작업보다 happens-before 관계에 있다면, A에서의 모든 메모리 변경 사항이 B에서 보인다
happens-before가 발생하는 규칙들
| 프로그램 순서 규칙 | 단일 스레드 내에서 코드 순서대로 실행 보장 |
|---|---|
| volatile 변수 규칙 | volatile 쓰기 → 그 변수를 읽는 모든 스레드에 보임 |
| 스레드 시작 규칙 | Thread.start() 이전 작업이 새 스레드 작업보다 먼저 보임 |
| 스레드 종료 규칙 | Thread.join() 반환 후, join 대상의 모든 작업이 보임 |
| 인터럽트 규칙 | Thread.interrupt() 호출이 인터럽트 감지보다 먼저 발생 |
| 객체 생성 규칙 | 생성자 완료 후 다른 스레드에서 참조 가능 |
| 모니터 락 규칙 | synchronized / ReentrantLock 블록 내 작업이 락 획득 스레드에 보임 |
| 전이 규칙 | A→B, B→C면 A→C도 성립 (Transitivity) |
전체 정리
개념 핵심 요약
| 인터럽트 | 대기 중인 스레드를 강제로 깨워 RUNNABLE로 전환. thread.interrupt() 호출 |
|---|---|
| runFlag 방식 | 간단하지만 sleep 중 반응 불가. 인터럽트와 함께 쓰면 보완 가능 |
| isInterrupted() | 인터럽트 상태 확인만. 상태 변경 없음 |
| Thread.interrupted() | 상태 확인 + true이면 false로 초기화. while 조건 체크 시 사용 권장 |
| InterruptedException | 발생 시 인터럽트 상태 자동으로 false 초기화 |
| Thread.yield() | CPU를 자발적으로 양보. RUNNABLE 상태 유지. OS에 힌트만 제공 |
| 메모리 가시성 | 멀티스레드 환경에서 한 스레드의 변경값이 다른 스레드에 언제 보이는지의 문제 |
| 캐시 메모리 | CPU 성능을 위해 코어별로 존재. 메인 메모리와 동기화 시점이 불확실 |
| volatile | 캐시를 건너뛰고 메인 메모리에 직접 읽기/쓰기. 가시성 보장, 약 5배 성능 저하 |
| JMM | 자바의 메모리 접근 규칙 명세. happens-before 관계가 핵심 |
| happens-before | A 완료 후 B에서 A의 변경값이 반드시 보임을 보장하는 관계 |
결론: volatile 또는 스레드 동기화 기법(synchronized, ReentrantLock) 을 사용하면 메모리 가시성 문제가 발생하지 않는다.
출처 : 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성
'JAVA' 카테고리의 다른 글
| [TIL] Java 멀티스레드 - LockSupport와 ReentrantLock (2026.05.06) (0) | 2026.05.06 |
|---|---|
| [TIL] Java 멀티스레드 - synchronized와 임계 영역 (2026.05.04) (0) | 2026.05.04 |
| [TIL] Java 멀티스레드 기초 정리 (2026.4.22) (1) | 2026.04.22 |
| getter 언제, 어떻게 사용해야 하는가!!! (0) | 2025.11.06 |
| [자료구조] 자료구조 기본 이해하기 (0) | 2025.11.04 |