RUNNABLE 상태인 Thread를 갑자기 정지하고 싶으면 interrupt를 사용하면 된다고 한다
interrupt가 어떻게 동작하는지 원리를 파헤쳐보고자 한다
✅ Thread 상태
Thread.State Enum을 확인해보면 아래와 같이 6개의 상태로 관리되고 있다
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW : 아직 Thread가 시작되지 않은 상태
- RUNNABLE : CPU Core에 의해 실행중이거나, 실행 준비가 된 상태
- 실행 상태 + OS 스케줄러의 실행 대기열에서 Context Switching 대기하는 상태
- BLOKCED : Thread가 동기화 락이 풀리기를 기다리는 상태 혹은 I/O로 인해 BLOCKED된 상태 (인터럽트가 걸려도, 상태를 빠져나오지 못함)
- WAITING : Thread가 무기한으로 다른 Thread가 작업을 기다리는 상태
- 아래의 메서드들이 호출되면 WAITING이 된다
- Object.wait()
- Thread.join() 등
- 아래의 메서드들이 호출되면 WAITING이 된다
- TIMED_WAITING : Thread가 특정 시간동안 다른 Thread 작업을 기다리는 상태
- 아래의 메서드들이 호출되면 TIMED_WAITING이 된다
- Thread.sleep(long timeout)
- Thread.wait(long timeout)
- join(long millis) 등
- 아래의 메서드들이 호출되면 TIMED_WAITING이 된다
- TERMINATED : Thread 실행 완료된 상태

✅ Join
특정 쓰레드가 TERMINATED될 때까지 WAITING 상태로 무기한 기다리게하는 메서드
public static void main(String[] args) throws InterruptedException {
Task task1 = new Task();
Task task2 = new Task();
Thread thread1 = new Thread(task1, "Task-1");
Thread thread2 = new Thread(task2, "Task-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}

- Task-1 thread가 RUNNABLE 상태로 전환되면 실행 혹은 스케줄러의 실행 대기열에 들어감
- Task-2 thread가 RUNNABLE 상태로 전환되면서 실행 혹은 스케줄러의 실행 대기열에 들어감
- 위의 두개의 쓰레드는 실행과 실행 대기열에서 대기를 반복함
- multi-core인 경우 병렬로(멀티프로세싱)
- single-core인 경우 context-switching 하면서(멀티테스킹)
- task1.join() 에 들어가면서, main Thread는 WAITING 상태로 전환
- task1이 TERMINATED 된다면, task2.join()에 걸리게 되어 task2도 TERMINATED 될 때까지 main Thread는 WAITING 상태
- task2.join()에서
🤔 join은 어떻게 main Thread에 Task Thread들이 TERMINATED 됐다고 알려줄까?
Thread.java
public final void join(long millis) throws InterruptedException {
if (millis < 0)
throw new IllegalArgumentException("timeout value is negative");
if (this instanceof VirtualThread vthread) {
if (isAlive()) {
long nanos = MILLISECONDS.toNanos(millis);
vthread.joinNanos(nanos);
}
return;
}
synchronized (this) {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else {
while (isAlive()) {
wait(0);
}
}
}
}
join메서드를 파라미터 없이 호출하면 join(0)이 호출되어 위의 메서드가 호출되게 된다
우리는 User Thread를 사용한 것이기 때문에 (Java 21부터 공시기 추가된 Virtual Thread는 추후 깊이 파볼 생각)
Thread.java
while(isAlive()) {
wait(0);
}
역시서 각 Task Thread들은 RUNNABLE 상태이면 지속적으로 wait(0)을 호출한다
isAlive는 무엇을 체크하는 걸까?
Thread.java
public final boolean isAlive() {
return alive();
}
/**
* Returns true if this thread is alive.
* This method is non-final so it can be overridden.
*/
boolean alive() {
return eetop != 0;
}
eetop가 0이 아닌 경우 Thread가 살아있다고 판단한다
private volatile long eetop;
volatile로 선언되어서 캐시 메모리를 거치지 않고 힙메모리에 바로 접근하게 해놓을정도로 중요해보이는 이 변수는
- 네이티브 쓰레드의 주소를 저장
- Java 쓰레드 모델은 Java의 유져 쓰레드와 OS의 네이티브 쓰레드를 1:1 매핑하는데 eetop가 중요한 역할을 하게됨
- https://dev-gyun.tistory.com/2
- Thread 시작시에 Non - Zero로 세팅되며
- Thread TERMINATED되면 0으로 세팅
wait는 무엇을 하는 것인가?
public final void wait(long timeoutMillis) throws InterruptedException {
long comp = Blocker.begin();
try {
wait0(timeoutMillis);
} catch (InterruptedException e) {
Thread thread = Thread.currentThread();
if (thread.isVirtual())
thread.getAndClearInterrupt();
throw e;
} finally {
Blocker.end(comp);
}
}
// final modifier so method not in vtable
private final native void wait0(long timeoutMillis) throws InterruptedException;
wait0은 Java Native Interface라서 소스를 볼수 없지만
java docs에 따르면
If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified.
Object (Java Platform SE 8 )
Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup. The general contract of fi
docs.oracle.com
Notify가 올때 까지 무기한 대기 (WAITING) 상태로 호출한 쓰레드를 만든다고 한다
즉, task1의 join()을 호출하게 되면
- task1이 alive한지 체크하는 while문 속에서 wait(0)을 호출하게 되고,
- while문이 끝날때까지 (task1이 TERMINATED될때까지)
- join()을 호출한 Main Thread가 WAITING 상태로 대기하는 것이다
✅ Interrupt
- WAITING, TIMED_WAITING, BLOCKED 상태의 Thread를 깨워서 Runnable 상태로 만드는 명령
- 특정 쓰레드를 중간에 중단(TERMINATED)시키는 명령
interrupt를 호출하면 해당 Thread(이하 Thread A)의 interrupted 변수를 true로 변경한다
Thread.java
public void interrupt() {
if (this != Thread.currentThread()) {
checkAccess();
// thread may be blocked in an I/O operation
synchronized (interruptLock) {
Interruptible b = nioBlocker;
if (b != null) {
interrupted = true;
interrupt0(); // inform VM of interrupt
b.interrupt(this);
return;
}
}
}
interrupted = true;
interrupt0(); // inform VM of interrupt
}
만약 interruptLock이 걸려있다면
- 즉, Thread A와 관련된 파일 입출력 작업이나 네트워크 작업을 하던 I/O 스레드가 Lock을 걸어놓은 상태라면
- I/O 쓰레드에게 Thread A에 대한 작업을 그만해도 된다고 interrupt(this)를 호출한 후에
- Thread A의 interrupted 변수를 true로 처리한다
Thread는 이 interrputed 변수가 참거짓인지를 보고 작업을 중단할지 판단한다
Thread.interrupted()는 interrupted 변수를 반환하고 interrupted를 false로 변경한다
Thread.java
public static boolean interrupted() {
return currentThread().getAndClearInterrupt();
}
boolean getAndClearInterrupt() {
boolean oldValue = interrupted;
// We may have been interrupted the moment after we read the field,
// so only clear the field if we saw that it was set and will return
// true; otherwise we could lose an interrupt.
if (oldValue) {
interrupted = false;
clearInterruptEvent();
}
return oldValue;
}
아래와 같이 interrupt의 상태를 확인하면서 Thread의 작업을 바로 종료할 수 있다
Thread thread = new Thread(() -> {
while(Thread.interrupted()) {
task();// 작업
}
});
thread.start();
thread.interrupt();
Sleep을 깨운다??
sleep 메서드 안을 들여다보면
Thread.java
public static void sleep(long millis) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
long nanos = MILLISECONDS.toNanos(millis);
ThreadSleepEvent event = beforeSleep(nanos);
try {
if (currentThread() instanceof VirtualThread vthread) {
vthread.sleepNanos(nanos);
} else {
sleep0(nanos);
}
} finally {
afterSleep(event);
}
}
- InterruptedException을 throw하는 것을 확인할 수 있고
virtualThread 일때는 vthread.sleepNanos 안에 아래와 같이 interrupt 인지 확인하고 InterruptException을 throw하는 것을 확인 할 수 있다
if (getAndClearInterrupt())
throw new InterruptedException();
즉 sleep 상태에서,
interrupted가 true이면(바로 interrupted를 false로 변경)
바로 Exception Throw되면서 TIME_WAITING 상태에서 RUNNABLE 상태로 변경된다
Thread 관련 공부를 하면서, 어떠한 방식으로 Thread 상태들이 관리되는지 궁금했는데
Java 코드들을 까보면서 그 원리에 대해서 얼추 이해해볼 수 있었다