스레드의 상태 종류는 무엇이 있고, 어떻게 제어할까?
스레드의 상태 종류
--
동작 흐름
- 스레드 객체 생성
=> "NEW" - start() 메서드를 통해 스레드 호출
=> 바로 실행되는 것이 아니라 "실행대기(RUNNABLE)"상태에서 자기 차례를 기다린다. - "실행대기"에서 자기 차례 도달
=> 자동으로 "실행(RUNNABLE)"상태로 이동하여 실행한다. - "실행" 상태에서 주어진 실행시간이 완료되거나 yield() 호출
=> 다시 "실행대기"로 돌아가 다시 차례를 기다린다. - "실행" 상태에서 도중에 [suspend(), sleep(), wait(), join(), I/O block 등]이 발생
=> "일시정지"상태로 이동하여 잠시 해당 작업을 보류한다. - "일시정지" 상태에 보류중인 작업들 중에 특정 작업에 대한 [time-out, resume(), notify(), interrupt()] 발생
=> 해당하는 특정 작업은 "일시정지"상태에서 빠져나와 다시 "실행대기"상태로 이동한다. - 모든 작업(실행)이 완료되거나 stop() 호출
=> 스레드 종료
실행 제어 메서드
static이 붙은 메서드들은 자기 자신의 스레드에서만 호출이 가능하다.
--
sleep()
--
현재 위치하고 있는 스레드를 지정된 시간 동안 잠시 멈추게 한다.
static void sleep(long millis) // 1/1000초
static void sleep(long millis, int nanos) // 1/1000초 + 나노초
static이기 때문에 sleep()을 사용하는 위치의 메서드에만 적용된다.
sleep() 메서드는
Thread 클래스 안에 static으로 선언되어 있기 때문에
해당 메서드를 사용하기 위해서는 Thread.sleep()으로 작성해야 한다.
사용 예시 코드
try {
Thread.sleep(1, 500000); // 0.0015초 동안 잠시 멈춤
} catch(InterruptedException e) { }
sleep() 메서드를 사용하면 예외가 발생하기 때문에 꼭 예외처리를 해줘야 한다.
(sleep() 메서드를 종료시키려면 "InterruptedException"예외를 발생시켜야 하기 때문이다.)
위 사용 예시 코드에서
catch에서 예외 처리 코드를 생략한 이유는
어떠한 문제가 발생해서 생긴 예외가 아니고, 그저 sleep() 상태를 빠져나가기 위해 발생한 예외이기 때문이다.
즉, catch블록에서는 그냥 아무것도 하지 않고 try-catch문을 빠져나가 이어서 진행하게 된다.
sleep() 상태에서 빠져나가는 방법 (= InterruptedException 예외 발생시키는 방법)
- time-up : 지정한 시간 종료
- interrupted : 강제 종료
응용 예시 코드
void delay(long millis) {
try {
Thread.sleep(millis);
} catch(InterruptedException e) { }
}
class ThreadTest {
public static void main(String args[]) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.start();
t2.start();
delay(2000);
System.out.print("<<main 종료>>");
}
static void delay(long millis) {
try {
t1.sleep(2000);
} catch(InterruptedException e) { }
}
}
delay(1000);
처럼 간편하게 현재 스레드를 잠시 멈추게 할 수 있다.
정리 예시 코드
class ThreadTest {
public static void main(String args[]) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.start();
t2.start();
try {
t1.sleep(2000);
} catch(InterruptedException e) { }
System.out.print("<<main 종료>>");
}
}
class MyThread1 extends Thread {
public void run() {
for(int i = 0; i < 300; i++) {
System.out.print("-");
}
System.out.print("<<t1 종료>>");
}
}
class MyThread2 extends Thread {
public void run() {
for(int i = 0; i < 300; i++) {
System.out.print("ㅣ");
}
System.out.print("<<t2 종료>>");
}
}
/* 결과
---------------------------------------------
-------ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ----ㅣㅣ---
-----ㅣㅣ---(생략)-----------------ㅣㅣㅣㅣㅣ
ㅣㅣㅣㅣ<<t1 종료>><<t2 종료>><<main 종료>>
*/
t1.sleep()으로 작성되어 있지만 t1스레드가 멈추지 않고 현재 스레드인 main스레드가 멈추게 된다.
즉, t1.sleep()이라고 작성했지만 Thread.sleep()으로 적용된다.
그래서 헷갈리지 않게 애초에 Thread.sleep()으로 작성하는 것이 좋다.
결과를 보면 main스레드가 2초 동안 멈춰 있는 사이에
t1, t2스레드가 먼저 종료된 것을 확인할 수 있다.
--
interrupt()
--
"대기(WAITING)"상태인 스레드를 "실행대기(RUNNABLE)"상태로 변경시킨다.
즉, 스레드가 "sleep()", "join()", "wait()"으로 인해 중지된 상태에서 다시 실행 가능한 상태로 만든다.
void interrupt() // 쓰레드의 interrupted상태를 false -> true로 변경
boolean isInterrupted() // 쓰레드가 interrupted가 되었는지 확인 및 반환
static boolean interrupted() // 쓰레드의 interrupted상태를 확인 및 반환 후 false로 변경
interrupted()는 static으로 선언되어 있어서 이 또한 현재 스레드에서만 사용가능하다.
( Thread.interrupted() )
예시 코드
class Test {
public static void main(String[] args) throws Exception {
Thread1 t1 = new Thread1();
t1.start();
String input = JOptionPane.showInputDiaLog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 : " + input + "입니다.");
t1.interrupt();
System.out.println("isInterrupted() : " + Thread.isInterrupted());
}
}
class Thread1 extends Thread {
public void run() {
int i = 10;
while(i != 0 && !isInterrupted()) {
System.out.println(i--);
try {
Thread.sleep(1000);
} catch(InterruptedException e) { }
}
System.out.println("카운트가 종료되었습니다.");
}
}
/* 결과
10
9
8
7
입력하신 값은 asdf입니다.
isInterrupted() : false
6
5
4
3
2
1
카운트가 종료되었습니다.
*/
위 예시 코드를 보면
값을 입력하면 t1.interrupt()가 호출되어 t1스레드의 interrupt값이 true로 되면서
while() 문의 조건에 충족하지 못하게 되어 탈출하게 되고 종료되는 것을 예상했지만,
값을 입력해도 타이머가 종료되지 않고 끝까지 while문이 동작한 뒤에 종료된다.
알아야 하는 정보 정리
- InterruptedException 예외가 발생하면 interrupt값이 자동으로 false로 초기화된다.
- interrupt()를 호출하여 interrupt값이 true로 변한다고 InterruptedException 예외가 발생하는 것은 아니다.
(대기상태에서 interrupt값이 true로 변하게 되면 그때 예외가 발생한다.)
평소에 스레드는 interrupt의 값은 false며, 대기상태에 진입해도 여전히 false다.
여기서 대기 상태를 빠져나가기 위해 interrupt()로 interrupt값을 true로 변경하고,
InterruptedException 예외가 발생하게 되며, 다시 자동으로 interrupt값이 false로 변경된다.
이를 비유하자면
interrupt는 어떤 상태를 의미하는 것이 아니라,
그저 특정 스레드를 대기상태에서 빠져나가게 하기 위해 잠깐 true로 변경하고 다시 false로 변경되는 트리거다.
마치 특정 작업을 수행하는 버튼처럼 꾹 누르고 있는 것이 아닌, 특정 이벤트를 위해 잠깐 눌렀다 때는 것이다.
(버튼을 누름 : interrupt = true, 버튼을 땐 상태 : interrupt = false)
대기 상태에 있는 스레드에게 interrupt()가 호출되면
자바 JVM은 해당 스레드에게 InterruptedException 예외를 던진다.
sleep() 동작은 지정한 시간이 종료되면 interrupt값이 true로 변경되는 것이 아닌
바로 InterruptedException 예외가 발생하는 방식으로 대기 상태를 빠져나간다.
동작 과정
- 평소에는 스레드의 interrupt값이 false이며, sleep()처럼 대기 상태에 진입을 해도 여전히 false다.
- 이 대기 상태를 빠져 가나기 위해 interrupt()를 호출하여 interrupt의 값을 true로 변경한다.
- 대기 상태에 있던 스레드는 InterruptedException 예외가 발생하여 catch() 문으로 이동하고,
다시 interrupt값은 false로 초기화된다. - 그래서 while문의 조건이 계속 충족하여 이어서 진행하게 된다.
물론 interrupt() 메서드를 호출하는 타이밍이 try-catch문이 끝났을 타이밍(sleep() 메서드가 끝난 타이밍)에
호출된다면 해당 스레드는 대기상태에 존재하지 않으므로 예외는 발생하지 않으며, interrupt값은 true로 유지된다.
즉, while문의 조건에 충족하지 않게 되어 탈출하고 타이머를 끝까지 수행하지 않고 스레드는 종료하게 된다.
해결한 예시 코드
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
interrupt();
}
InterruptedException 예외가 발생하면 interrupt값이 false로 다시 변경되지만
catch문 안에서 다시 interrupt()를 호출하여 true로 변경하면 while문 조건에서 탈출할 수 있다.
--
suspend(), resume(), stop()
--
void suspend() // 쓰레드를 일시정지 시킨다.
void resume() // suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 만든다.
void stop() // 쓰레드를 즉시 종료시킨다.
- suspend() : 일시 정지
- resume() : 일시 정지를 다시 재개
- stop() : 완전 정지(종료)
위 3개의 메서드는 "deprecated"로 되어 있어서 사용을 권장하지 않는다.
스레드의 동작을 강제로 억제하는 것이기 때문에 "교착 상태"를 일으킬 가능성이 있기 때문이다.
--
join()
--
지정된 시간 동안 특정 스레드(다른 스레드)가 작업을 하도록 기다린다.
void join() // 작업이 모두 끝날 때까지
void join(long millis) // 1/1000초 동안 기다림
void join(long millis, int nanos) // 1/1000초 + 나노초 동안 기다림
join() 또한 sleep()처럼 대기상태로 진입하는 것이기 때문에
try-catch문으로 InterruptedException 예외 처리를 해야 한다.
예시 코드
class Test {
static long startTime = 0;
public static void main(String args[]) {
ThreadEx1 t1 = new TreadEx1();
TrheadEx2 t2 = new TreadEx2();
t1.start();
t2.start();
startTime = System.currentTimeMillis();
try {
t1.join();
t2.join();
} catch(InterruptedException e) { }
System.out.print("소요시간 : " + (System.currentTimeMillis() - ThreadEx.startTime));
}
}
class ThreadEx1 extends Thread {
public void run() {
for(int i = 0; i < 300; i++) {
System.out.print(new String("-"));
}
}
}
class ThreadEx2 extends Thread {
public void run() {
for(int i = 0; i < 300; i++) {
System.out.print(new String("ㅣ"));
}
}
}
--------------------------------------------------------------------
----------ㅣㅣㅣㅣㅣㅣ---(생략)----ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ
ㅣㅣㅣㅣㅣㅣㅣ----------ㅣㅣㅣㅣㅣㅣㅣㅣ-------소요시간 : 9
main 스레드는 t1, t2가 모두 끝날 때까지 대기상태에 있다가
끝나고 나서야 이어서 main스레드가 진행된다.
--
yield()
--
현재 자신(스레드)에게 할당된 실행 시간을 중간에 다음 차례의 스레드에게 양보하고 자신은 다시 실행대기로 이동한다.
5초라는 작업시간을 할당받았지만
3초만 사용하고 2초는 굳이 필요 없어서 다음 스레드에게 2초를 양보한다.
static void yield()
yield() 또한 static 메서드로, 자신에게만 사용이 가능하다.
예시 코드
class Test implements Runnable {
boolean suspended = false;
boolean stopped = false;
Thread th;
MyThreadEx(String name) {
th = new Thread(this, name);
}
public void run() {
while (!stopped) {
if (!suspended) {
// 쓰레드가 수행할 코드 작성
try {
Thread.sleep(1000);
} catch(InterruptedException e) { }
} else {
Thread.yield();
}
}
}
public void start() {
th.start();
}
public void resume() {
suspended = false;
}
public void suspend() {
suspended = true;
th.interrupt();
}
public void stop() {
stopped = true;
th.interrupt();
}
}
사실 yield()는 OS스케줄러에게 통보만 하는 것으로
반드시 yield()가 동작한다는 보장은 없다.
그래서 사실 yield()를 작성한 경우와 하지 않았을 경우는 크게 차이가 없다.
(그래도 사용하는 편이 조금이나마 빠른 편)
yield()와 interrupt()를 적절히 사용하면, 응답성과 효율을 높일 수 있다.
--
'Language > Java' 카테고리의 다른 글
람다식 (+ 메서드 참조, 함수형 인터페이스) (0) | 2025.01.12 |
---|---|
스레드의 동기화 (0) | 2025.01.09 |
스레드 (Thread) (0) | 2024.12.29 |
제네릭 (Generics) (0) | 2024.12.28 |
Arrays 클래스에서 제공하는 메서드 (0) | 2024.12.25 |