백엔드 엔지니어 이재혁
[Java] Thread 본문
- Main 함수에서 Thread 객체를 생성
Thread th1 = new Thread()); - 해당 Thread의 start 메서드 실행
th1.start(); - Thread가 새로 자신만의 스택 프레임을 생성
- Thread가 run() 메서드를 실행
@Override public void run() { System.out.println(Thread.currentThread().getName() + ": run()"); }
start()로 thread를 시작해줘야 thread가 실제로 자신만의 스레드 작업 공간을 생성하고 스레드로서 역할을 한다.
th1.run() 을 실행하면 안된다. run() 메서드를 main에서 직접 호출하면 main 스레드가 th1의 run() 메서드를 직접 실행한다. 단일 스레드로 실행됨.
스레드 사용 주의점
1. 실행 순서는 얼마든지 달라질 수 있다!
2. 실행 기간을 보장하지 않는다!
Java 종료 시점
main 스레드가 종료된다고 Java가 종료되지 않는다. 만약 다른 사용자 스레드가 실행되고 있다면, main이 종료되어도 다른 사용자 스레드가 모두 종료될 때까지 Java는 살아있다.
데몬(Daemon) 스레드
데몬 스레드라고 설정된 스레드들은 아직 실행 중이어도 무시하고 종료할 수 있다.
다음과 같은 방식으로 데몬 스레드를 설정할 수 있는데, 반드시 스레드를 start()하기 전에 설정해줘야 한다.
daemonThread.setDaemon(true); // 데몬 스레드 여부
특이점
Run() 메서드는 체크 예외를 던질 수 없다. RuntimeException은 던질 수 있다.
간단하게 설명하면, Runnable 인터페이스의 Run() 메서드는 Exception을 던지도록 되어 있지 않기 때문이다.
그 이유는 Java에서 Exception을 설계한 것에 연관되어 있는데, 오버라이딩 규칙 상 부모 메소드가 선언하지 않은 Checked Exception은 자식 메소드에서 던질 수 없기 때문이다. 왜냐면 체크 예외는 모두 프로그래머가 직접 핸들해주기를 바라고 설계를 했기 때문이다. 만약 부모 클래스가 던지는 예외보다 상위 예외를 던지게 되면, 다형성과 결합시 예외 처리를 못해주는 상황이 발생할 수 있다. 실제로 문제가 발생하는 코드를 보고 싶으면 아래 더 보기 확인.
```java
class Parent {
void method() throws InterruptedException {
// ...
}
}
class Child extends Parent {
@Override
void method() throws Exception {
// ...
}
}
public class Test {
public static void main(String[] args) {
Parent p = new Child();
try {
p.method();
} catch (InterruptedException e) {
// InterruptedException 처리
}
}
}
위의 상황에서는 Child.method()의 Exeption을 main()의 catch에서 잡아주지 못한다.try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
이런식으로 Thread.sleep();을 쓸 때 try~catch로 직접 Exception을 잡아줘야 한다.
Thread의 상태
[Thread].getState() 로 현재 상태를 확인할 수 있음
상태의 종류
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED_WAITING
- TERMINATED
이 부분이 중요하기 때문에 상태만 보고 어떤 상태인지 설명할 수 있는지 확인하자!
[Thread].join()
어떤 Thread의 join() 메서드를 호출하면 해당 Thread가 Terminated 상태가 될 때까지 해당 메서드를 호출한 Thread는 대기 (WAITING) 상태가 된다. 만약 join(long ms) 메서드를 호출하면 최대 ms 밀리초만큼 대기(TIMED_WAITING)하고, 그래도 끝나지 않는 경우 그냥 대기 상태를 멈추고 다시 Thread를 CPU 스케쥴러에 넣는다.
참고: join()도 인터럽트 대상이다.
[Thread].interrupt()
Thread에 인터럽트를 실행하는 메서드이다.
인터럽트를 실행하면 Thread는 RUNNABLE 상태가 되며 작업을 계속 진행한다. 그리고 InterruptedException을 던지는 메서드를 만났을 때 인터럽트 익셉션을 던지고 catch에서 받는다.
그렇다면, InterruptedException을 던지지 않는 작업이 장기간 이어질 경우에는 어떻게 되는가?
Thread가 계~속 작업을 실행하며, 그런 작업이 완료될 때까지 InterruptedException을 처리해줄 catch 문이 실행되지 않는다.
그런 경우에도 강제로 종료하고 싶다면, flag를 직접 활용해서 상태값을 조건문으로 추적해야 한다. (추후 Thread.interrupted()를 사용하면 현재 interrupt된 상태인지 체크 가능하다는 것을 배움)
하지만 또 이런 경우가 발생할 수 있다.
InterruptedException을 만나기 전까지는 계속 isInterrupted 상태가 true로 유지된다는 점을 유의해야 한다. 조건문을 빠져나오더라도 다음에 만나는 InterrupedException을 던지는 메서드를 만나면 그 Exception을 발동시킨다. 나는 지금 조건문만 빠져나오기를 바랬는데, InterrupedException을 던지는 메서드를 제대로 실행하지 못하고, catch문으로 넘어가게 된다.
간단히 정리하면, 인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 돌려두어야 한다는 점을 인터럽트 사용시에 유념해 둬야 한다.
Thread.interrupted()를 사용하면 현재 interrupt 상태를 확인해줄 뿐 아니라, true 인 경우 그 값을 false로 다시 바꿔주는 기능까지 같이 해준다. 위에서 말한 내용으로 정리하면, 인터럽트 상태를 다시 정상(false)으로 돌려주는 역할까지 한다는 것이다.
현재 궁금증
Thread에서 대기 중인 여러 메서드들을 전부 건너뛰고 interrupt를 여러번 주고 싶으면 어떻게 해야하지?
다음과 같은 작업 예시에서 궁금함
static class MyTask implements Runnable {
@Override
public void run() {
while (!Thread.interrupted()) { // 인터럽트 상태 변경 O
log("작업 중");
}
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
try {
log("자원 정리");
Thread.sleep(1000);
log("자원 정리 종료");
} catch (InterruptedException e) {
log("자원 정리 실패! - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
}
// 인터럽트가 여러번 필요한 경우에는???
log("sleep 2 실행");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log("인터럽트2 터짐");
}
log("sleep 3 실행");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log("인터럽트3 터짐");
}
log("sleep 4 실행");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log("인터럽트4 터짐");
}
log("작업 종료");
}
}
main 스레드에서 thread.interrupt()를 여러번 연속으로 실행해도 결국 main 스레드의 interrupt 요청들이 MyTask의 InterruptedException 이후 코드들보다 빠르게 실행이 되어서 잘 안되던데
Thread.yield()
CPU 스케쥴러에게 다른 스레드에게 작업을 양보해줘도 된다고 힌트를 주는 것이다. (최종 결정은 CPU 스케쥴러가 함)
아래 코드와 같이 중요도가 작은 반복적인 확인 작업에 대해 yield를 추가해주는 것도 방법이다.
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); // 추가
continue;
}
// ...
}
정말 중요도가 많이 낮다면 Thread.sleep()으로 강제로 쉬도록 만드는 것도 방법
'Java' 카테고리의 다른 글
| [Java] 생산자 소비자 문제 (0) | 2025.05.26 |
|---|---|
| [Java] concurrent.locks (0) | 2025.05.23 |
| [Java] synchronized (0) | 2025.05.22 |
| [Java] 메모리 가시성 (0) | 2025.05.22 |
| [Java] Runnable (0) | 2025.05.19 |