인터럽트 - 시작1
특정 스레드의 작업을 중간에 중단하려면 어떻게 해야할까?
package thread.control.interrupt;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class ThreadStopMainV1 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(4000);
log("작업 중단 지시 runFlag = true");
task.runFlag = false;
}
static class MyTask implements Runnable {
volatile boolean runFlag = true;
@Override
public void run() {
while(runFlag) {
log("작업 중");
sleep(3000);
}
log("자원 정리");
log("자원 종료");
}
}
}
- work 스레드는 runFlag 가 true 인 동안 계속 실행된다.
프로그램 시작 4초 뒤에 main 스레드는 runFlag 를 false 로 변경한다.
work 스레드는 while(runFlag) 에서 runFlag 의 조건이 false 로 변한 것을 확인하고 while 문을 빠져나가면서 작업을 종료한다.
인터럽트 - 시작2
인터럽트를 사용하면 , WAITING, TIMED_WAITTINF 같은 대기 상태의 스레드를 직접 깨워서, 작동하는 RUNNABEL 상태로 만들 수 있다.
package thread.control.interrupt;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class ThreadStopMainV2 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(4000);
log("작업 중단 지시 thread.interrupt()");
thread.interrupt();
log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
static class MyTask implements Runnable {
@Override
public void run() {
try {
while(true) {
log("작업 중");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
log("interrupt message = " + e.getMessage());
log("state = " + Thread.currentThread().getState());
}
log("자원 정리");
log("자원 종료");
}
}
}
인터럽트 - 시작3
그런데 앞선 코드에서 한가지 아쉬운 부분이 있다
while (true) { //인터럽트 체크 안함
log("작업 중");
Thread.sleep(3000); //여기서만 인터럽트 발생
}
다음과 같이 인터럽트의 상태를 확인하면, 더 빨리 반응할 수 있을 것이다.
while (인터럽트_상태_확인) { //여기서도 인터럽트 상태 체크
log("작업 중");
Thread.sleep(3000); //인터럽트 발생
}
이 코드와 같이 인터럽트의 상태를 확인하면 while문을 체크하는 부분에서 더 빠르게 while문을 빠져나갈 수 있다. 물론 이 예제의 경우 코드가 단순해서 실질적인 차이는 매우 작다.
추가로 인터럽트의 상태를 직접 확인하면, 다음과 같이 인터럽트를 발생 시키는 sleep() 과 같은 코드가 없어도 인터럽트 상태를 직접 확인하기 때문에 while문을 빠져나갈 수 있다
while (인터럽트_상태_확인) { //여기서도 체크
log("작업 중");
}
package thread.control.interrupt;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class ThreadStopMainV3 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(100); // 시간을 줄임
log("작업 중단 지시 thread.interrupt()");
thread.interrupt();
log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
static class MyTask implements Runnable {
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {
log("작업 중");
}
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
try {
log("자원 정리");
Thread.sleep(1000);
} catch (InterruptedException e) {
log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " +
Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
여기까지 보면 아무런 문제가 없어 보인다. 하지만 이 코드에는 심각한 문제가 있다.
바로 ` work` 스레드의 인터럽트 상태가 ` true` 로 계속 유지된다는 점이다.
앞서 인터럽트 예외가 터진 경우 스레드의 인터럽트 상태는 ` false` 가 된다.
반면에 ` isInterrupted()` 메서드는 인터럽트의 상태를 변경하지 않는다.
단순히 인터럽트의 상태를 확인만 한다.
` work` 스레드는 이후에 자원을 정리하는 코드를 실행하는데, 이때도 인터럽트의 상태는 계속 ` true` 로 유지된다.
이때 만약 인터럽트가 발생하는 ` sleep()` 과 같은 코드를 수행한다면, 해당 코드에서 인터럽트 예외가 발생하게 된다
. 이것은 우리가 기대한 결과가 아니다! 우리가 기대하는 것은 ` while()` 문을 탈출하기 위해 딱 한 번만 인터럽트를 사용 하는 것이지, 다른 곳에서도 계속해서 인터럽트가 발생하는 것이 아니다.
- 자바에서 인터럽트 예외가 한 번 발생하면, 스레드의 인터럽트 상태를 다시 정상( ` false` )으로 돌리는 것은 이런 이유 때문이다.
- 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하게 된다.
- 인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 돌려두어야 한다.
인터럽트 - 시작4
Thread.interrupted()
스레드의 인터럽트 상태를 단순히 확인만 하는 용도라면 isInterrupted() 를 사용하면 된다
하지만 직접 체크해서 사용할 때는` Thread.interrupted() 를 사용해야 한다.
이 메서드는 다음과 같이 작동한다
- 스레드가 인터럽트 상태라면 true 를 반환하고, 해당 스레드의 인터럽트 상태를 false 로 변경한다.
- 스레드가 인터럽트 상태가 아니라면 false 를 반환하고, 해당 스레드의 인터럽트 상태를 변경하지 않는다.
package thread.control.interrupt;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class ThreadStopMainV4 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(100); // 시간을 줄임
log("작업 중단 지시 thread.interrupt()");
thread.interrupt();
log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
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);
} catch (InterruptedException e) {
log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " +
Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
자바는 인터럽트 예외가 한 번 발생하면, 스레드의 인터럽트 상태를 다시 정상( ` false ` )으로 돌린다.
스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하게 된다.
인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 돌려두어야 한다.
프린터 예제1 - 시작
package thread.control.interrupt;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class MyPrinterV1 {
public static void main(String[] args) {
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "printer");
printerThread.start();
Scanner userInput = new Scanner(System.in);
while (true) {
log("프린터할 문서를 입력하세요. 종료 (q): ");
String input = userInput.nextLine();
if(input.equals("q")){
printer.work = false;
break;
}
printer.addJob(input);
}
}
static class Printer implements Runnable {
volatile boolean work = true;
Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
@Override
public void run() {
while (work) {
if (jobQueue.isEmpty()){
continue;
}
String job = jobQueue.poll();
log("출력 시작 " + job + " , 대기 문서 " + jobQueue);
sleep(3000); // 출력에 걸리는 시간
log("출력 완료 " + job);
}
log("프린터 종료");
}
public void addJob(String input) {
jobQueue.offer(input);
}
}
}
- main 스레드 : 사용자의 입력을 받아서 Printer 인스턴스의 jobQueue에 담는다.
- printer 스레드 : jobQueue 가 있는지 확인한다.
- jobQueue 에 내용이 있으면 poll() 을 이용해서 꺼낸 다음에 출력한다.
- 출력하는 데 약 3 초 시간이 걸린다. 여기서 sleep(3000) 를 사용해서 출력 시간을 가상으로 구현했다.
- 출력을 완료하며 while 문을 다시 반복한다.
- 만약 jobQueue 가 비었다면 continue 를 사용해서 다시 while 문을 반복한다.
- 이렇게 jobQueue 에 출력한 내용이 들어올 때 까지 계속 확인한다.
인터럽트 적용! & - yield 도입
package thread.control.interrupt;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;
import static util.MyLogger.log;
public class MyPrinterV3 {
public static void main(String[] args) {
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "printer");
printerThread.start();
Scanner userInput = new Scanner(System.in);
while (true) {
log("프린터할 문서를 입력하세요. 종료 (q): ");
String input = userInput.nextLine();
if(input.equals("q")){
printerThread.interrupt();
break;
}
printer.addJob(input);
}
}
static class Printer implements Runnable {
volatile boolean work = true;
Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
@Override
public void run() {
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()){
Thread.yield(); // 추가
continue;
}
String job = jobQueue.poll();
log("출력 시작 " + job + " , 대기 문서 " + jobQueue);
try {
Thread.sleep(3000); // 출력에 걸리는 시간
} catch (InterruptedException e) {
log("인터럽트!");
break;
}
log("출력 완료 " + job);
}
log("프린터 종료");
}
public void addJob(String input) {
jobQueue.offer(input);
}
}
}
'JAVA' 카테고리의 다른 글
[JAVA] String intern() (1) | 2024.11.15 |
---|---|
[JAVA] 동기화 - synchronized (1) | 2024.11.13 |
[JAVA] 스레드 Join (0) | 2024.11.12 |
[JAVA] Runnable 을 만드는 다양한 방법 (0) | 2024.11.11 |
[JAVA] 데몬 스레드 (0) | 2024.11.11 |