Java Thread 2부 - Thread 제어하기
1부에서는 Thread가 무엇인지 알아봤다.
핵심은 간단했다.
Thread는 코드를 실행하는 흐름이다.
이번 글에서는 실행 중인 Thread를 어떻게 제어하는지 알아본다.
이번 2부에서 다룰 내용은 다음과 같다.
Thread 상태
sleep()join()yield()interrupt()
1. Thread는 상태를 가진다
Thread는 생성되고, 실행되고, 기다리고, 종료된다.
즉, Thread도 생명주기를 가진다.
자바에서 Thread 상태는 Thread.State로 확인할 수 있다.
대표적인 상태는 다음과 같다.
상태 | 의미 |
|---|---|
| Thread 객체만 생성된 상태 |
| 실행 가능한 상태 |
| 락을 얻기 위해 기다리는 상태 |
| 다른 Thread의 신호를 기다리는 상태 |
| 정해진 시간 동안 기다리는 상태 |
| 실행이 끝난 상태 |
처음부터 전부 외울 필요는 없다.
지금은 이렇게만 기억하면 된다.
생성됨 → 실행 가능 → 대기 또는 실행 → 종료NEW 상태
Thread 객체를 만들기만 하면 NEW 상태다.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Thread 실행");
});
System.out.println(thread.getState());
}
}실행 결과는 다음과 같다.
NEW아직 start()를 호출하지 않았기 때문에 Thread는 실행되지 않았다.
RUNNABLE 상태
start()를 호출하면 Thread는 실행 가능한 상태가 된다.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
// 작업 중
}
});
thread.start();
System.out.println(thread.getState());
}
}실행 결과는 보통 다음과 비슷하다.
RUNNABLERUNNABLE은 말 그대로 실행 가능한 상태다.
중요한 점은 RUNNABLE이라고 해서 반드시 지금 CPU에서 실행 중이라는 뜻은 아니다.
실행 중일 수도 있고, 실행 순서를 기다리는 중일 수도 있다.
TERMINATED 상태
Thread의 작업이 끝나면 TERMINATED 상태가 된다.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("Thread 실행");
});
thread.start();
thread.join();
System.out.println(thread.getState());
}
}실행 결과는 다음과 같다.
Thread 실행
TERMINATEDjoin()은 잠시 뒤에 설명한다.
여기서는 Thread 실행이 끝나면 TERMINATED가 된다는 것만 보면 된다.
2. sleep() - 잠깐 멈추기
sleep()은 현재 실행 중인 Thread를 일정 시간 동안 멈춘다.
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("시작");
Thread.sleep(1000);
System.out.println("1초 후 실행");
}
}실행 결과는 다음과 같다.
시작
1초 후 실행Thread.sleep(1000)은 현재 Thread를 1초 동안 쉬게 한다.
여기서 현재 Thread는 main Thread다.
sleep()은 현재 Thread를 멈춘다
이 부분이 중요하다.
sleep()은 특정 Thread 객체를 멈추는 메서드처럼 보이지만, 실제로는 현재 실행 중인 Thread를 멈춘다.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("worker 실행");
});
thread.sleep(1000);
thread.start();
}
}이 코드는 thread가 쉬는 것이 아니다.
thread.sleep(1000)처럼 호출했지만, 실제로 쉬는 것은 현재 실행 중인 main Thread다.
그래서 보통 이렇게 작성한다.
Thread.sleep(1000);sleep()은 static 메서드이기 때문이다.
TIMED_WAITING 상태
sleep() 중인 Thread는 TIMED_WAITING 상태가 된다.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("Thread가 깨워짐");
}
});
thread.start();
Thread.sleep(500);
System.out.println(thread.getState());
}
}실행 결과는 다음과 같다.
TIMED_WAITING정해진 시간 동안 기다리고 있기 때문에 TIMED_WAITING 상태가 된다.
3. join() - 다른 Thread가 끝날 때까지 기다리기
join()은 특정 Thread가 끝날 때까지 현재 Thread를 기다리게 한다.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("worker: " + i);
}
});
worker.start();
System.out.println("worker가 끝날 때까지 대기");
worker.join();
System.out.println("worker 종료 후 main 실행");
}
}실행 결과는 다음과 같다.
worker가 끝날 때까지 대기
worker: 1
worker: 2
worker: 3
worker 종료 후 main 실행worker.join()을 호출하면 main Thread는 worker Thread가 끝날 때까지 기다린다.
즉, join()은 실행 순서를 맞출 때 사용한다.
join()이 없으면 어떻게 될까?
다음 코드를 보자.
public class Main {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("worker 완료");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
worker.start();
System.out.println("main 종료");
}
}실행 결과는 보통 다음과 같다.
main 종료
worker 완료main이 먼저 끝나도 worker는 계속 실행된다.
이번에는 join()을 추가해보자.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("worker 완료");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
worker.start();
worker.join();
System.out.println("main 종료");
}
}실행 결과는 다음과 같다.
worker 완료
main 종료join() 덕분에 main Thread가 worker Thread의 종료를 기다린다.
4. yield() - 양보하기
yield()는 현재 Thread가 CPU 사용을 잠시 양보하겠다는 의미다.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("worker: " + i);
Thread.yield();
}
});
thread.start();
for (int i = 1; i <= 5; i++) {
System.out.println("main: " + i);
}
}
}하지만 중요한 점이 있다.
yield()는 반드시 양보된다는 보장이 없다.
운영체제 스케줄러에게 “다른 Thread를 실행해도 좋다”고 힌트를 주는 정도다.
그래서 yield()에 의존해서 실행 순서를 제어하면 안 된다.
실무에서는 자주 사용하지 않는다.
5. interrupt() - Thread에게 중단 요청하기
Thread를 강제로 죽이는 것은 위험하다.
그래서 자바에서는 보통 interrupt()로 중단을 요청한다.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("작업 중...");
}
System.out.println("중단 요청 감지");
});
worker.start();
Thread.sleep(1000);
worker.interrupt();
}
}interrupt()는 Thread를 즉시 죽이지 않는다.
대신 해당 Thread에게 “멈춰야 할 수도 있다”는 신호를 보낸다.
Thread는 isInterrupted()를 통해 그 신호를 확인할 수 있다.
sleep() 중 interrupt가 오면?
sleep() 중인 Thread에 interrupt()가 호출되면 InterruptedException이 발생한다.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
System.out.println("잠들기");
Thread.sleep(5000);
System.out.println("깨어남");
} catch (InterruptedException e) {
System.out.println("자는 중에 interrupt 발생");
}
});
worker.start();
Thread.sleep(1000);
worker.interrupt();
}
}실행 결과는 다음과 같다.
잠들기
자는 중에 interrupt 발생interrupt()는 잠들어 있거나 기다리는 Thread를 깨울 수 있다.
그래서 오래 대기하는 작업을 안전하게 중단할 때 자주 사용된다.
InterruptedException을 무시하면 안 된다
다음 코드는 좋지 않다.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 아무것도 안 함
}왜냐하면 interrupt 요청을 그냥 먹어버리기 때문이다.
최소한 다음처럼 처리하는 것이 좋다.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Thread.currentThread().interrupt()를 다시 호출하면 interrupt 상태를 복구할 수 있다.
이렇게 해야 상위 코드에서도 “이 Thread에 중단 요청이 있었구나”를 알 수 있다.
핵심 정리
Thread 2부의 핵심은 이것만 기억하면 된다.
Thread는 여러 상태를 가진다.
NEW는 아직 시작 전 상태다.RUNNABLE은 실행 가능한 상태다.TERMINATED는 실행이 끝난 상태다.sleep()은 현재 Thread를 잠깐 멈춘다.join()은 다른 Thread가 끝날 때까지 기다린다.yield()는 CPU 사용을 양보하는 힌트지만 보장되지 않는다.interrupt()는 Thread를 강제로 죽이는 것이 아니라 중단 요청을 보내는 것이다.InterruptedException을 무시하면 안 된다.
Thread 제어에서 가장 중요한 건 두 가지다.
join() → 기다리기
interrupt() → 중단 요청하기다음 글에서는 여러 Thread가 같은 데이터를 사용할 때 생기는 문제를 다룬다.
바로 Race Condition과 synchronized다.