자바 스레드 생명 주기
자바 스레드의 생명 주기는 시작부터 시작하여 실행, 일시 중지, 종료의 상태를 갖고 있다.

1. New - 스레드가 생성되고 아직 시작되지 않은 상태
2. Runnable - 실행될 준비가 됐거나, 실제로 실행 중인 상태
3. Blocked - 동기화 락을 기다리는 상태, synchronized에서만 사용하는 특별한 스레드 대기 상태
4. Waiting - 다른 스레드의 작업이 완료되기를 무기한 기다리는 상태
5. Timed Waiting - 스레드가 일정 시간 동안 대기하는 상태
6. Terminated - 스레드의 실행이 완료된 상태
join - 다른 스레드의 작업 대기
class SumTask implements Runnable {
int startValue;
int endValue;
int result = 0;
public SumTask(int startValue, int endValue) {
this.startValue = startValue;
this.endValue = endValue;
}
@Override
public void run() {
sleep(2000);
int sum = 0;
for (int i = startValue; i <= endValue; i++) {
sum += i;
}
result = sum;
}
}
2초 뒤에 1부터 10까지 더하는 Runnable 구현체가 있다.
SumTask task = new SumTask(1, 10);
Thread thread = new Thread(task, "SumTask-1");
thread.start();
System.out.println(task.result); //0 출력
다음과 같이 스레드에 로직을 할당해서 실행하면 결과는 예상과 달리 0이 나온다.
이유는 main 스레드가 해당 스레드의 작업을 기다리지 않고 작업의 결과물을 출력하기 때문이다.
SumTask task = new SumTask(1, 10);
Thread thread = new Thread(task, "SumTask-1");
thread.start();
// 작업이 끝날때가지 무기한 대기
thread.join();
System.out.println(task.result); //55 출력
join() 메서드를 통해 스레드의 작업이 끝날 때까지 main 스레드를 대기시킬 수 있다.
이 때 main 스레드의 상태는 Waiting이다.
SumTask task = new SumTask(1, 10);
Thread thread = new Thread(task, "SumTask-1");
thread.start();
// 작업이 끝날 때가지 1초간 대기
thread.join(1000);
System.out.println(task.result); //0 출력
다음과 같이 시간을 지정하면, 해당 시간 동안만 대기하게 된다. main 스레드의 상태는 Timed Waiting이다.
인터럽트
'작업을 중단해야 한다'는 신호를 보내는 메커니즘이다.
class SumTask implements Runnable {
@Override
public void run() {
log("작업 진행");
int result = 0;
for (int i = 0; i <= 10; i++) {
result += i;
}
log.info("작업 완료 결과: {}", result);
//스레드 인터럽트 상태 확인
log.info("work 완료 후 스레드 인터럽트 상태 = {}", Thread.currentThread().isInterrupted());
}
}
SumTask에는 1부터 10까지 더하는 로직이 있다.
isInterrupted() 메서드를 통해 현재 스레드의 인터럽트 상태를 확인할 수 있다.
SumTask task = new SumTask();
Thread thread = new Thread(task, "work");
thread.start();
log.info("인터럽트 신호 발생");
thread.interrupt();
스레드에 할당하여 실행한 후, 인터럽트를 발생하면 어떻게 될까? 왠지 바로 신호를 받고 멈출 것 같다.

예상과는 달리 스레드의 상태만 변경이 되었고, 스레드의 작업 자체는 멈추지 않았다.
인터럽트는 스레드를 강제로 종료시키지 않는다. 단지 "중단 요청"만 보내어 인터럽트 상태만 변경할 뿐, 실제로 어떻게 대응할지는 개발자가 직접 코드로 작성해야 하는 것이다.
그렇다면 인터럽트를 어떻게 대응할 수 있을까?
1. InterruptedException 예외를 통한 중단 로직 작성
InterruptedException은 인터럽트 상태가 true일 때 스레드가 Waiting 혹은 Timed Waiting과 같은 대기 상태로 진입하거나,
또는 이미 대기 상태인 스레드에게 인터럽트 요청이 오면 발생하는 예외이다.
//InterruptedException을 밖으로 던짐
public static void sleep(long millis) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
..
}
대표적으로 Thread의 sleep 함수를 보면, InterruptedException을 메서드 밖으로 throws 하는 것을 볼 수 있다.
이 외에도 스레드의 상태를 대기 상태로 변경하는 join() 메서드도 InterruptedException을 발생시킬 수 있다.
class SumTask implements Runnable {
@Override
public void run() {
int result = 0;
for (int i = 0; i <= 10; i++) {
//스레드를 0.01초 대기시킴
try {
Thread.sleep(10);
} catch (InterruptedException e) {
//인터럽트 발생 시, 로직 중지
break;
}
result += i;
}
log("작업 완료 결과: " + result);
log("work 완료 후 스레드 인터럽트 상태 = " + Thread.currentThread().isInterrupted());
}
}
SumTask에서 sleep 함수를 통해 잠시동안 대기시키고, InterruptedException이 발생하면 로직을 중지시키는 방식으로 인터럽트 요청을 처리할 수 있다.
이 방법은 InterruptedException을 발생시키기 위해서 스레드를 일정 주기마다 대기 상태로 변경해야 한다는 단점이 있다.
2. interrupted() - 인터럽트 상태 체크
class SumTask implements Runnable {
@Override
public void run() {
int result = 0;
for (int i = 0; i <= 10; i++) {
// if(Thread.currentThread().isInterrupted()) //인터럽트 상태 : true -> true
if(Thread.interrupted()) { //인터럽트 상태 : true -> False
break;
}
result += i;
}
log("작업 완료 결과: " + result);
log("work 완료 후 스레드 인터럽트 상태 = " + Thread.currentThread().isInterrupted());
}
}
interrupted() 메서드를 통해서 인터럽트 상태를 확인하여 스레드의 상태 변경 없이 인터럽트 요청을 처리할 수 있다.
interrupted() 메서드로 인터럽트 상태가 true일 때를 찾게 되면, 상태는 알아서 false로 변경해 준다.
isInterrupted() 메서드는 정말 단순히 인터럽트 상태를 체크하기 위한 용도이다.
해당 메서드로 인터럽트 요청을 처리하려고 한다면, 인터럽트가 정상으로 돌아가지 않고 이후에 계속해서 인터럽트가 발생하게 된다.
yield - 다른 스레드에게 작업 순서 양보
스레드가 바쁘지 않다면, 다른 스레드에게 CPU 제어권을 넘겨주고 싶을 수 있다.
yield() 메서드를 통해서 현재 스레드의 CPU 제어권을 다른 스레드에게 넘길 수 있다.
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
============
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
log(" - " + i);
//CPU 제어권 넘김
Thread.yield();
}
}
}
위 코드에서 스레드는 로그를 출력한 후 다른 스레드에게 제어권을 넘긴다.
sleep()
- 다른 스레드에게 CPU 제어권을 양보하지 않음
- 상태가 Timed Waiting으로 변경
yield()
- 다른 스레드에게 CPU 제어권을 양보하거나, 본인 스레드가 계속 실행
- 상태는 Runnable 유지
volatile - 메모리 가시성
예시
class MyWork implements Runnable {
boolean runFlag = true;
@Override
public void run() {
log("task 시작");
while (runFlag) {
// runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}
flag가 false로 변하면 무한루프를 탈출하는 Runnable 구현체가 있다.
MyWork work = new MyWork();
Thread t = new Thread(work, "work"); //runFlag = true
t.start();
sleep(1000);
//무한루프 탈출 시도
work.runFlag = false;
log("runFlag = " + work.runFlag); //runFlage = false
log("main 종료");
스레드 생성 후 로직을 실행시키다가 flag 값을 false로 주어서 무한루프를 탈출시키려고 한다.

하지만 결과를 보면, main 스레드는 종료되었지만 "task 종료"라는 로그가 출력되지 않은 것을 볼 수 있으며, 실제로 무한루프가 계속 돌고 있어서 프로세스는 실행 상태가 지속된다.
스레드 메모리 구조

스레드는 각자의 캐시 메모리를 갖고 있다.
main 스레드에서 변경한 runFlag 값은 main 스레드의 캐시 메모리에만 적용이 된 것이고, work 스레드 캐시 메모리에는 동기화되지 않은 것이다.
main 스레드에서 변경된 값이 메인 메모리에 언제 동기화될지 알 수 없으며, 메인 메모리의 변경 내역이 언제 work 스레드에 반영되는지 알 수 없다.
멀티 스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 동기화될지에 대한 문제를 메모리 가시성이라고 한다.
해결
해결은 간단하다. 여러 스레드가 읽고 쓰는 값이 있다면 volatile 키워드를 사용하면 된다.
class MyWork implements Runnable {
//volatile 키워드를 통해 메모리 가시성 해결
volatile boolean runFlag = true;
@Override
public void run() {
log("task 시작");
while (runFlag) {
// runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}

키워드를 적용함으로써 캐시 메모리를 사용하지 않고 곧바로 메인 메모리로 향하는 것을 볼 수 있다.
캐시 메모리를 사용하지 않기 때문에 이전보다 느려진다는 단점이 있다.
'Language > Java' 카테고리의 다른 글
| [Java] 자바 스레드 풀과 ExecutorService (0) | 2026.01.27 |
|---|---|
| [Java] ReentrantLock을 통한 스레드 동기화 (0) | 2026.01.02 |
| [Java] Stream Collector, Downstream Collector (0) | 2025.12.26 |
| [Java] Stream API와 지연 연산 (0) | 2025.12.25 |
| [Java] 메서드 참조 (0) | 2025.12.24 |