스레드 상태 제어 - 기본
스레드의 상태 제어를 이해하기 위해서는 스레드 라이프사이클(생명주기)에 대한 이해가 필요합니다. 다음 그림은 자바의 스레드 상태 흐름을 도식화한 그림입니다.
스레드 상태의 흐름을 자바 코드로 설명드리겠습니다. 스레드의 상태는 Thread 클래스 내에 State라는 이름의 enum으로 정의되어 있습니다. 그리고 getState() 메소드를 호출하면 해당 스레드의 상태를 확인할 수 있습니다.
NEW
스레드 인스턴스를 생성해주면 스레드는 NEW 상태가 됩니다.
public static void main(String[] args) throws Exception {
Thread thread = new Thread();
System.out.println(thread.getState());
}
출력 결과:
NEW
RUNNABLE
스레드 인스턴스를 생성 이후, 해당 스레드를 시작하면 RUNNABLE 상태가 됩니다.
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() ->
System.out.println(Thread.currentThread().getState()));
thread.start();
}
출력 결과:
RUNNABLE
RUNNING
스레드가 RUNNABLE 상태가 되면 CPU 스케줄링 방식에 따라 RUNNABLE 상태의 여러 스레드들이 CPU를 점유하면서 실행됩니다. CPU 스케줄링은 컴퓨팅 시스템과 관련이 있기 때문에 RUNNING 상태는 없습니다.
BLOCKED
자바 객체의 모니터 락(혹은 고유 락)에 대한 이해가 필요한데, 간단히 설명하면 여러 스레드가 공유된 자원에 동시에 접근하여 값을 읽고 변경하게 되면 변경이 순차적으로 처리되길 바라겠지만 그렇지 못한 경우가 발생합니다. 이러한 문제를 해결하기 위해 자바는 모든 객체가 락을 갖고 있고 어느 객체에 접근할 때 해당 객체의 락을 획득한 스레드만이 접근이 가능해집니다. 아래 코드에서 synchronized 블록은 모니터 락을 이용하여 스레드의 접근을 제어합니다. 자세한 설명은 아래 링크를 참고하길 바랍니다.
여러 스레드가 한 클래스에 접근하려 한다면 한개의 스레드만이 락을 획득하여 해당 클래스에 접근하게 되는데, 다른 스레드들은 BLOCKED 상태가 되고 그 클래스에 접근하기 위해 대기합니다. 해당 클래스의 락을 획득한 스레드가 그 클래스에 대한 처리가 완료되면 락을 해제하는데, 이 때, BLOCKED 상태인 스레드가 있으면 락을 획득하여 스레드 상태가 RUNNABLE 상태로 바뀌고 그 클래스에 대한 처리를 진행합니다.
// 공유 자원
public static class Counter {
private int count;
// 이 메서드를 여러 스레드에서 접근하려 할 때,
// 락을 획득한 스레드만이 접근 가능해지고
// 다른 스레드는 BLOCKED 상태로 바뀌고 접근하기 위해 대기합니다.
public synchronized int increase() {
return ++count;
}
}
public static void main(String[] args) throws Exception {
Thread mainThread = Thread.currentThread();
Counter counter = new Counter();
Thread thread = new Thread(() -> {
Thread thisThread = Thread.currentThread();
while(true) {
int num = counter.increase();
System.out.println(
"otherThread[" +
mainThread.getName() + ": " +
mainThread.getState() + ", " +
thisThread.getName() + ": " +
thisThread.getState() + ", " +
"value: " + num + "]");
if(num > 10)
break;
}
}, "other");
thread.start();
while(true) {
int num = counter.increase();
System.out.println(
"mainThread[" +
mainThread.getName() + ": " +
mainThread.getState() + ", " +
thread.getName() + ": " +
thread.getState() + ", " +
"value: " + num + "]");
if(num > 10)
break;
}
}
출력 결과:
mainThread[main: RUNNABLE, other: RUNNABLE, value: 1]
otherThread[main: RUNNABLE, other: RUNNABLE, value: 2]
otherThread[main: BLOCKED, other: RUNNABLE, value: 4]
otherThread[main: BLOCKED, other: RUNNABLE, value: 5]
mainThread[main: RUNNABLE, other: RUNNABLE, value: 3]
otherThread[main: BLOCKED, other: RUNNABLE, value: 6]
mainThread[main: RUNNABLE, other: RUNNABLE, value: 7]
otherThread[main: RUNNABLE, other: RUNNABLE, value: 8]
mainThread[main: RUNNABLE, other: BLOCKED, value: 9]
otherThread[main: BLOCKED, other: RUNNABLE, value: 10]
mainThread[main: RUNNABLE, other: BLOCKED, value: 11]
otherThread[main: BLOCKED, other: RUNNABLE, value: 12]
WAITING
BLOCKED나 WAITING 상태는 모두 스레드 동기화와 관련이 있습니다. 자바에서 동기화 처리는 모니터 락으로 처리하고 대기중인 스레드의 상태를 BLOCKED나 WAITING으로 바꿔줍니다. BLOCKED는 위에서 설명한 것처럼 공유 자원에 접근할 때, 스레드 한 개씩 접근할 수 있도록 접근된 스레드 이외의 스레드는 BLOCKED 상태로 바꿔주고 대기합니다. 반면 WAITING은 스레드끼리 동기화가 필요할 때, 대기하는 스레드의 상태를 WAITING으로 바꿔주고 대기가 끝나면 RUNNABLE 상태로 바뀝니다. 기본적으로는, 자바 Object 객체의 wait() 메소드와 notify() 혹은 notifyAll() 메소드로 구현합니다.
wait() 메소드, notify() 메소드 등에 대한 내용은 다음 글을 참고하시길 바랍니다.
스레드가 WAITING 상태로 바뀌는 경우는 다음 3가지 경우입니다.
Object.wait()
Thread.join()
Thread 클래스의 join() 메소드는 해당 스레드가 종료(TERMINATED)될 때까지 WAITING 상태로 대기를 하고 대기가 끝나면 RUNNABLE 상태로 돌아갑니다. join() 메소드도 wait() 등의 메소드로 구현된 것입니다.
public static void main(String[] args) throws Exception { Thread mainThread = Thread.currentThread(); Thread thread = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // main 스레드는 이 스레드의 종료를 기다리고 있기 때문에 // 상태가 WAITING입니다. System.out.println("2. " + mainThread.getName() + ": " + mainThread.getState()); }, "other"); thread.start(); System.out.println("1. " + thread.getName() + ": " + thread.getState()); thread.join(); System.out.println("3. " + thread.getName() + ": " + thread.getState()); }
출력 결과:
1. other: RUNNABLE 2. main: WAITING 3. other: TERMINATED
LockSupport.park()
TIMED_WAITING
스레드의 구현 코드 내에서(run() 메소드) sleep() 메소드가 호출되면 해당 스레드는 TIMED_WAITING 상태가 됩니다. sleep 시간이 만료되면 해당 스레드는 다시 RUNNABLE 상태가 됩니다.
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
Thread thisThread = Thread.currentThread();
try {
Thread.sleep(2000); // 스레드 상태가 TIMED_WAITING로 바뀝니다.
System.out.println("2. " + thisThread.getName() + ": " + thisThread.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "other");
thread.start();
Thread.sleep(1000);
System.out.println("1. " + thread.getName() + ": " + thread.getState());
}
출력 결과:
1. other: TIMED_WAITING
2. other: RUNNABLE
TERMINATED
스레드가 TERMINATED 상태가 되는 경우는 두 가지입니다.
스레드 내의 모든 코드 처리가 완료되었을 때(run() 메소드 내에 있는 코드 실행 완료)
public static void main(String[] args) throws Exception { Thread thread = new Thread("other"); thread.start(); Thread.sleep(1000); System.out.println(thread.getName() + ": " + thread.getState()); }
출력 결과:
other: TERMINATED
stop() 메소드를 호출하여 강제 종료시킬 때
stop() 메소드는 호출은 가능하지만 스레드를 강제적으로 종료시키기 때문에 자원들이 불안전한 상황에 놓일 수 있어서 안전하지 않습니다. 그래서 현재 해당 메소드는 Deprecated되었습니다.
스레드를 안전하게 종료시키기 위해서는 interrupt() 메소드를 이용해야 합니다. interrupt() 메소드는 스레드가 실행중(RUNNABLE 혹은 RUNNING)일 때는 영향이 없고 대기 상태(WAITING, TIMED_WAITING, BLOCKED)가 되었을 때, InterruptedException 예외를 발생시킵니다.
public static void main(String[] args) throws Exception { Thread thread = new Thread(() -> { try { // for문 안에서는 // 스레드 상태가 RUNNABLE 혹은 RUNNING이기 때문에 // interrupt하더라도 // InterruptedException 예외가 발생하지 않습니다. for (int i = 0; i < 100; i++) { if(i == 50) break; } // sleep 메소드가 실행되면 // 스레드의 상태가 TIMED_WAITING 상태로 바뀌어서 // interrupt하게 되면 // InterruptedException 예외가 발생하고 종료됩니다. Thread.sleep(1000); } catch (InterruptedException e) {} }); thread.start(); Thread.sleep(100); thread.interrupt(); Thread.sleep(100); System.out.println(thread.getName() + ": " + thread.getState()); }
출력 결과:
other: TERMINATED
정리
마지막으로, 위 내용 중 중요한 부분을 정리하겠습니다.
스레드 실행: RUNNABLE(RUNNING)
스레드 대기: BLOCKED, WAITING, TIMED_WAITING
스레드가 실행되고 어느 상황에 대기 상황의 상태로 바뀌고 다시 실행 상태로 바뀌는지 이를 정교하고 개발하는 것이 가장 스레드 상태 제어의 핵심이라고 볼 수 있습니다. 그중 문제는 RUNNABLE -> BLOCKED, WAITING 이 부분에서 가장 많은 문제가 발생할 여지가 있습니다.
다음 글에서는 심화적인 내용으로 어떠한 문제가 발생할 수 있는지 해결방안은 무엇이 있는지 살펴보겠습니다.
- 스레드 종료: TERMINATED
스레드가 종료될 때, 정상적으로 스레드가 실행이 완료되어 종료된다면 큰 문제가 없습니다. 하지만 부득이하게 강제 종료를 시켜야하는 경우가 있습니다. 자원의 안전성에 문제가 발생할 수 있는데 그 경우에는 이 것만 기억하면 됩니다.
[X]스레드 실행 -> 스레드 종료(RUNNABLE -> TERMINATED)
[O]스레드 대기 -> 스레드 종료(BLOCKED, WAITING, TIMED_WAITING -> TERMINATED)
따라서, 스레드의 강제 종료는 stop() 메소드를 사용하지 않고 interrupt() 메소드로 스레드에 예외를 발생시켜서 종료되게 합니다.
'Java' 카테고리의 다른 글
[JAVA] 스레드 생성 - Thread 클래스와 Runnable 인터페이스 (0) | 2021.06.14 |
---|
최근댓글