Thread의 스케줄링
스레드의 스케줄링 관련 메서드로는
sleep()
, join()
, interrupt()
, yield()
, stop()
, suspend()
, resume()
이 있는데,
이 중 stop()
, suspend()
, resume()
은
스레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated 되어 사용하지 않는다.
Multithreading 프로세스에서
여러 스레드가 같은 프로세스 내의 자원을 공유하며 생기는 문제점을 방지하기 위해
한 스레드가 진행 중인 작업을 다른 스레드가 간섭하지 못하게 막고,
이를 스레드의 동기화(synchronization)라 한다.
하지만 두 개 이상의 스레드가 동시에 같은 데이터에 접근하고 데이터를 변경하면
원하는 결과를 얻지 못하는 경우가 발생할 수 있다.
wait()
과 notify()
는 스레드의 동기화를 통해
공유되는 데이터를 보호하면서 하나의 스레드가 작업을 끝낸 뒤 다른 스레드가 작업을 하도록 만든다.
이러한 Thread와 관련된 메서드를 정리하면 다음과 같다.(deprecated 메서드 제외)
static void sleep(long millis) |
- 지정된 시간(millisecond)동안 스레드 일시정지
- 지정된 시간이 지나면 실행대기 상태로 자동 전환
|
void join() void join(long millis) |
- 스레드 자신이 하던 작업을 멈추고 다른 스레드가 지정된 시간동안 작업을 수행하도록 함
- 시간을 지정하지 않으면 해당 스레드가 작업을 모두 마칠 때까지 일시정지
|
void wait() void wait(long timeoutMillis) |
- 임계영역 내 코드를 수행하다가 일시정지 후 notify()의 호출을 기다림
- 지정된 시간이 있는 경우 해당 시간동안만 기다림
- Object class에 정의되어 있음
- 동기화블럭 내에서만 사용 가능
|
void notify()
void notifyAll() |
- 코드 수행이 완료되었음을 알려 다른 스레드에게 lock을 넘김
- waiting pool에서 기다리고 있는 임의의 스레드 하나에게 알림
- notifyAll()은 기다리고 있는 모든 스레드에게 알림 (lock은 하나의 스레드만 얻을 수 있음)
- Object class에 정의되어 있음
- 동기화블럭 내에서만 사용 가능
* waiting pool은 객체마다 존재함 * notifyAll()이 호출된 객체의 waiting pool에서 대기 중인 스레드만 대상으로 함
|
static void yield() |
- 스레드 자신에게 주어진 실행시간을 다음 차례의 스레드에게 양보함
|
void interrupt() |
- 스레드의 interrupted 상태를 false에서 true로 변경
|
boolean isInterrupted() |
- 스레드의 interrupted 상태를 반환
|
static boolean interrupted() |
- 현재 스레드의 interrupted 상태를 반환한 후 false로 변경
|
여러 가지 메서드 중 이번에 살펴보고자 하는 내용을 요약하면 이렇게 정리할 수 있다.
- sleep() / join() / wait() : 스레드를 일시정지 상태로 변경
- interrupt() : sleep(), join(), wait()에 의해 일시정지된 스레드를 실행대기 상태로 변경
그중에서도 interrupt()
의 작동 방식을 조금 더 자세히 정리해보고자 한다.
Interrupt()
- sleep()
, join()
, wait()
에 의해 일시정지 상태인 스레드를 실행대기 상태로 전환
// InterruptedException이 발생하여 일시정지 상태 해제
- sleep()
, join()
, wait()
중 하나의 호출로 인해 블로킹된 경우,
해당 스레드의 interrupted 상태가 초기화(false)되고 InterruptedException 발생
// 상태 초기화의 이유: interrupt()
가 여러 번 발생하는 상황일 때
interrupted 상태를 감지하려면 false 상태여야 가능
// catch블럭 내 코드를 수행 후 try - catch 구문을 빠져나감
// 일시정지 상태에서 interrupt()
를 사용하면 interrupt signal은 바로 보내지만,
실제로 이 signal을 받고 예외를 발생시키는 건 해당 스레드가 스케줄러의 선택을 받고 실행되는 시점임
- interrupted()
가 호출되었을 때 스레드가 일시정지 상태가 아니라면 아무 일도 발생하지 않음
예시
조금 복잡해 보이지만 예시를 통해 살펴보면 이해가 쉽다.
0부터 1씩 3초 간격으로 증가하는 th1 스레드가 있고, 메인은 실행 후 6초 뒤 종료되는 코드이다.
th1을 interrupt 하여 스레드를 정지시키고자 한다.
public class ThreadEx {
public static void main(String[] args) {
ThreadNumAdd th1 = new ThreadNumAdd();
th1.start();
try {
Thread.sleep(6000);
} catch (InterruptedException e) {}
th1.interrupt();
System.out.println("main stopped");
}
}
class ThreadNumAdd extends Thread {
public void run() {
for (int i = 0; !isInterrupted(); i++) {
System.out.println(i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("thread stopped");
}
}
}
}
Result
0
1
2
main stopped
thread stopped
3
4
하지만 결과를 보면 스레드를 정지시킨 이후에도 계속해서 실행되고 있는데,
이 코드의 문제는 스레드가 잠들어 있을 때 interrupt 되어 예외가 발생되었다 해도
이후 반복 실행 조건을 확인하는 과정에서 !isInterrupted() == true
로 처리되어
무한반복에 빠진다는 것이다.
이 문제는 다음과 같이 해결할 수 있다.
1. catch블럭에 return;을 작성하여 예외 발생 시 반복문을 종료하도록 함
class ThreadNumAdd extends Thread {
public void run() {
for (int i = 0; !isInterrupted(); i++) {
System.out.println(i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("thread stopped");
return;
}
}
}
}
2. 반복문을 try블럭 안에 넣어 예외발생 시 바로 반복문을 탈출하도록 함
class ThreadNumAdd extends Thread {
public void run() {
int i = 0;
try {
while (!isInterrupted()) {
System.out.println(i++);
Thread.sleep(3000);
}
} catch (InterruptedException e) {
System.out.println("thread stopped");
}
}
}
3. 기존 코드에서 catch블럭 안에 interrupt();
를 추가하여 !isInterrupted == false
로 만듦
class ThreadNumAdd extends Thread {
public void run() {
for (int i = 0; !isInterrupted(); i++) {
System.out.println(i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
interrupt();
System.out.println("thread stopped");
}
}
}
}
세 가지 방법 모두 "thread stopped" 출력 이후 프로그램이 종료된다.
공부자료: 자바의 정석