Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- 스프링 db2 - 데이터 접근 기술
- jpa - 객체지향 쿼리 언어
- 자바 중급1편 - 날짜와 시간
- 자바의 정석 기초편 ch5
- 스프링 mvc1 - 스프링 mvc
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch12
- 2024 정보처리기사 수제비 실기
- 자바 중급2편 - 컬렉션 프레임워크
- 자바 기본편 - 다형성
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch6
- 스프링 입문(무료)
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch11
- 스프링 mvc2 - 로그인 처리
- 2024 정보처리기사 시나공 필기
- 게시글 목록 api
- 스프링 mvc1 - 서블릿
- 스프링 db1 - 스프링과 문제 해결
- 스프링 고급 - 스프링 aop
- @Aspect
- jpa 활용2 - api 개발 고급
Archives
- Today
- Total
나구리의 개발공부기록
스레드 제어와 생명 주기, 인터럽트, 프린터 예제, yield - 양보하기, 프린터 예제에 yield 도입 본문
인프런 - 실전 자바 로드맵/실전 자바 - 고급 1편, 멀티스레드와 동시성
스레드 제어와 생명 주기, 인터럽트, 프린터 예제, yield - 양보하기, 프린터 예제에 yield 도입
소소한나구리 2025. 2. 10. 21:15728x90
출처 : 인프런 - 김영한의 실전 자바 - 고급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 인터럽트
1) 스레드의 작업을 중간에 중단
(1) ThreadStopMainV1
- 특정 스레드의 작업을 중단하는 가장 쉬운 방법은 변수를 사용하는 것임
- 여기서는 runFlag를 사용해서 work 스레드에 작업 중단을 지시할 수 있음
- 작업 하나에 3초가 걸린다고 가정하고 sleep(3000)을 사용하였고 main 스레드에서 4초 뒤에 runFlag를 false로 변경하여 작업 중단을 지시함
- volatile 키워드는 뒤에서 자세히 설명하는데 지금은 단순히 여러 스레드에서 공유하는 값에 사용하는 키워드라는 정도로 알고 넘어가면 됨
- 프로그램을 실행해보면 시작 후 4초 뒤에 main 스레드가 작업 중단 지시를 내리고 프로그램이 종료되는 것을 확인할 수 있음
package thread.control.interrupt;
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=false");
task.runFlag = false;
}
static class MyTask implements Runnable {
volatile boolean runFlag = true;
@Override
public void run() {
while (runFlag) {
log("작업 중");
sleep(3000);
}
log("자원 정리");
log("자원 종료");
}
}
}
/* 실행 결과
16:48:06.328 [ work] 작업 중
16:48:09.334 [ work] 작업 중
16:48:10.321 [ main] 작업 중단 지시 runFlag=false
16:48:12.340 [ work] 자원 정리
16:48:12.341 [ work] 자원 종료
*/
(2) 실행 분석
- work 스레드는 runFlag가 true인 동안 계속 실행됨
- 프로그램 시작 후 4초 뒤에 main 스레드는 runFlag를 false로 변경하므로 2번째 while문이 실행되는 중간에 변경됨
- work 스레드는 while(runFlag)에서 runFlag의 조건이 false로 변한 것을 확인하고 while문을 빠져 나가면서 작업을 종료함
(3) 문제점
- 실행을 해보면 바로 알겠지만 main 스레드가 runFlag=false를 통해 작업 중단을 지시해도 work 스레드가 즉각 반응하는 것이 아니라 작업 중단 지시 2초 정도 이후에 자원과 작업을 종료하는 로그를 확인할 수 있음
- 이 방식의 가장 큰 문제는 while문 내부의 sleep()에 있음
- main 스레드가 runFlag를 false로 변경해도 work 스레드는 sleep(3000)을 통해 3초간 잠들어있음
- 지금 상황에서는 3초간의 잠이 깬 다음에 while(runFlag) 코드를 실행해야 runFlag를 확인하고 작업을 중단할 수 있으므로 스레드가 대기하는 상태에서 스레드를 깨우는 방법이 필요함
** 참고
- runFlag를 변경한 후 2초라는 시간이 지난 이후에 작업이 종료되는 이유는 work 스레드가 3초에 한 번씩 깨어나서 runFlag를 확인하는데 main 스레드가 4초에 runFlag를 변경했기 때문임
- work 스레드 입장에서 보면 두 번째 sleep()에 들어가고 1초 후 main 스레드가 runFlag를 변경했으므로 2초가 더 있어야 깨어남
2) 인터럽트 사용
(1) ThreadStopMainV2
- 인터럽트를 사용하면 WAITING, TIMED_WAITING 같은 대기 상태의 스레드를 직접 깨워서 작동하는 RUNNABLE 상태로 만들 수 있음
- 여기에서는 인터럽트를 이해하기 위해 직접 만든 sleep()대신 Thread.sleep()을 사용하고 try-catch로 예외를 처리하였음
- 특정 스레드의 인스턴스에 interrupt() 메서드를 호출하면, 해당 스레드에 인터럽트가 발생하고 인터럽트가 발생하면 해당 스레드에 InterruptedException이 발생함
- 이때 인터럽트를 받은 스레드는 대기 상태에서 깨어나 RUNNABLE 상태가 되고 코드를 정상 수행하며 InterruptedException을 catch로 잡아서 정상 흐름으로 변경하면 됨
- interrupt()를 호출했다고 해서 즉각 InterruptedException이 발생하는 것이 아니라 sleep()처럼 InterruptedException을 던지는 메서드를 호출하거나 또는 호출 중일 때 예외가 발생함
- 즉, 아래의 코드에서 while(true), log("작업 중")에서는 InterruptedException이 발생하지 않고 Thread.sleep() 처럼 InterruptedException을 던지는 메서드도 호출하거나 호출하며 대기 중일 때 예외가 발생함
- 실행 결과를 확인해보면 thread.interrupt()를 통해 작업 중단을 지시하고 거의 즉각적으로 인터럽트가 발생한 것을 확인할 수 있음
- 이때 work 스레드는 TIMED_WAITING -> RUNNABLE 상태로 변경되면서 InterruptedException 예외가 발생하고 catch의 코드 블럭으로 이동하여 정상흐름으로 코드를 수행함
- work 스레드가 catch블럭 안에서 출력한 상태를 보면 RUNNABLE 상태로 바뀐 것을 확인할 수 있음
package thread.control.interrupt;
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("자원 종료");
}
}
}
/* 실행 결과
17:17:42.726 [ work] 작업 중
17:17:45.732 [ work] 작업 중
17:17:46.720 [ main] 작업 중단 지시 thread.interrupt()
17:17:46.721 [ main] work 스레드 인터럽트 상태1 = true
17:17:46.722 [ work] work 스레드 인터럽트 상태2 = false
17:17:46.722 [ work] interrupt message = sleep interrupted
17:17:46.722 [ work] state = RUNNABLE
17:17:46.723 [ work] 자원 정리
17:17:46.723 [ work] 자원 종료
*/
(2) 실행 분석
- main 스레드가 4초 뒤에 work 스레드에 interrupt()를 걸면 work 스레드는 인터럽트 상태(true)가 됨
- 스레드가 인터럽트 상태일 때는, sleep() 처럼 InterruptedException이 발생하는 메서드를 호출하거나 또는 이미 호출하고 대기 중이라면 InterruptedException이 발생함
- 이때 2가지 일이 발생함
- work 스레드는 TIME_WAITING 상태에서 RUNNABLE 상태로 변경되고 InterruptedException 예외를 처리하면서 반복문을 탈출하며 work 스레드가 인터럽트 상태이기 때문에 인터럽트 예외가 발생함
- 인터럽트 상태에서 인터럽트 예외가 발생하면 work 스레드는 다시 작동하는 상태가 되므로 work 스레드의 인터럽트 상태는 종료됨(false)
- 인터럽트가 적용되고 인터럽트 예외가 발생한 후 해당 스레드는 실행 가능 상태가 되고 인터럽트 발생 상태도 정상으로 돌아옴
- 인터럽트를 사용하면 대기 중인 스레드를 바로 깨워서 실행 가능한 상태로 바꿀 수 있어 runFlag를 사용하는 이전 방식보다 반응성이 좋아졌음
3) 개선 시도
(1) 아쉬운 점
- 앞선 코드는 while(true)의 부분은 체크를 하지 않기 때문에 인터럽트가 발생해도 다음 코드로 넘어가고 sleep()을 호출하고 나서야 인터럽트가 발생함
- 만약 while 문을 체크하는 부분에서 인터럽트의 상태를 확인하면 인터럽트 상태를 더 빨리 반응할 수 있으므로 더 빠르게 while문을 빠져나갈 수 있음
- 추가로 while문에서 인터럽트의 상태를 직접 확인하면 인터럽트를 발생 시키는 sleep()과 같은 코드가 없어도 인터럽트 상태를 직접 확인하기 때문에 while문을 빠져나갈 수 있음
- 물론 지금 예제의 경우 코드가 단순하기 때문에 실질적인 차이는 매우 적음
(2) ThreadStopMainV3
- while문에 인터럽트의 상태를 직접 확인하고 run() 반복문에서 sleep() 코드도 제거한 후 코드를 실행해보면 작업 중단 지시 이후에 빠르게 작업이 종료되는 것을 확인할 수 있음
- 그러나 실행 결과를 보면 V2 버전과의 차이가 있는데, V2에서는 catch문을 거쳐서 자바가 스스로 인터럽트의 상태를 false로 변경해주었지만 지금은 작업 중단이 되었음에도 인터럽트의 상태가 여전히 true로 되어있는 것을 확인할 수 있음
- main 스레드는 interrupt() 메서드를 사용해서 work 스레드에 인터럽트를 걸면 work 스레드는 인터럽트 상태이므로 while문의 조건이 false가 되면서 while문을 탈출함(!true -> false)
- 여기까지만 보면 인터럽트가 true가 되어도 아무 문제가 없어보이지만 매우 심각한 문제가 있음
package thread.control.interrupt;
public class ThreadStopMainV3 {
public static void main(String[] args) {
// ... 기존 코드 동일
sleep(100); // 0.1초 뒤에 작업 중단 지시
// ... 기존 코드 동일
}
static class MyTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
log("작업 중");
}
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
log("자원 정리");
log("자원 종료");
}
}
}
/* 실행 결과 일부
...
17:49:44.293 [ work] 작업 중
17:49:44.293 [ work] 작업 중
17:49:44.293 [ main] 작업 중단 지시 thread.interrupt()
17:49:44.296 [ main] work 스레드 인터럽트 상태1 = true
17:49:44.296 [ work] work 스레드 인터럽트 상태2 = true
17:49:44.296 [ work] 자원 정리
17:49:44.296 [ work] 자원 종료
*/
(3) ThreadStopMainV3 - 추가
- work 스레드는 이후에 자원을 정리하는 코드를 실행하는데 이때도 인터럽트의 상태는 계속 true로 유지하게 됨
- 이때 만약 인터럽트가 발생하는 sleep()과 같은 코드가 수행된다면 해당 코드에서 인터럽트 예외가 발생하게 됨
- 실제 기대하는 동작은 while()문을 탈출하기 위해 딱 한 번만 인터럽트를 사용하는 것이지 다른 곳에서도 계속해서 인터럽트가 발생하는 것은 기대하는 결과가 아님
- 결과적으로 실행 결과를 보면 자원 정리 중에 InterruptedException이 발생하여 정상적인 자원 종료가 되지 않는 것을 확인할 수 있음
- 자바에서 인터럽트 예외가 한 번 발생하면, 스레드의 인터럽트 상태를 다시 정상(false)로 돌리는 이유가 이런 이유 때문임
- 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하게 되므로 인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 돌려 두어야 함
package thread.control.interrupt;
public class ThreadStopMainV3 {
// ... 기존 코드 동일 생략
}
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);
log("자원 종료");
} catch (InterruptedException e) {
log("자원 정리 실패 = 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태 3 = " + Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
/* 실행 결과 일부
18:06:29.432 [ work] 작업 중
18:06:29.432 [ work] 작업 중
18:06:29.432 [ main] 작업 중단 지시 thread.interrupt()
18:06:29.432 [ work] 작업 중
18:06:29.435 [ main] work 스레드 인터럽트 상태1 = true
18:06:29.435 [ work] work 스레드 인터럽트 상태2 = true
18:06:29.436 [ work] 자원 정리
18:06:29.436 [ work] 자원 정리 실패 = 자원 정리 중 인터럽트 발생
18:06:29.436 [ work] work 스레드 인터럽트 상태 3 = false
18:06:29.436 [ work] 작업 종료
*/
4) 최종 개선
(1) ThreadStopMainV4
- isInterrupted(): 스레드의 인터럽트 상태를 단순히 확인할 때 사용
- Thread.interrupted(): 직접 체크해서 사용할 때 사용
- 이 메서드는 스레드가 인터럽트 상태라면 true를 반환하는데 해당 스레드의 인터럽트 상태를 false로 변경하고, 스레드가 인터럽트 상태가 아니라면 false를 반환하고 스레드의 인터럽트 상태를 변경하지 않음
- while문의 조건에 Thread.interrupted()를 사용하여 인터럽트 상태가 true일때 false로 자동으로 변경되도록하여 코드를 실행해보면 중간에 InterruptedException을 발생하는 코드가 존재해도 정상적으로 자원이 종료되는 것을 확인할 수 있음
package thread.control.interrupt;
public class ThreadStopMainV4 {
public static void main(String[] args) {
// ... 기존 코드 동일 생략
}
static class MyTask implements Runnable {
@Override
public void run() {
while (!Thread.interrupted()) { // 인터럽트 상태가 true이면 false로 변경됨
// ... 기존 코드 동일 생략
}
}
/* 실행 결과
18:18:40.594 [ work] 작업 중
18:18:40.594 [ main] 작업 중단 지시 thread.interrupt()
18:18:40.594 [ work] 작업 중
18:18:40.597 [ work] work 스레드 인터럽트 상태2 = false
18:18:40.597 [ work] 자원 정리
18:18:40.597 [ main] work 스레드 인터럽트 상태1 = false
18:18:41.602 [ work] 자원 종료
18:18:41.603 [ work] 작업 종료
*/
(2) 정리
- Thread.interrupted()를 호출했을 때 스레드가 인터럽트 상태라면 true를 반환하고 해당 스레드의 인터럽트 상태를 false로 변경하여 while문을 탈출하는 시점에 스레드의 인터럽트 상태도 false로 변경되었음
- 그 결과 자원을 정리하는 코드를 실행하는데 인터럽트가 발생하는 코드가 수행되어도 인터럽트가 발생하지 않아 자원을 정상적으로 잘 정리하는 것을 확인할 수 있음
- 자바는 인터럽트 예외가 한번 발생하면 스레드의 인터럽트 상태를 다시 정상으로 돌리며, 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하기 때문에 인터럽트의 목적을 달성하면 상태를 다시 정상으로 돌려 두어야 함
- 인터럽트의 상태를 직접 체크해서 사용하는 경우 Thread.interrupted()를 사용하면 이런 부분을 해결할 수 있으며 isInterrupted()는 스레드의 상태를 변경하지 않고 확인할 때 사용하면 됨
- 이것은 모든 상황에서의 정답은 아닐 수 있는데, 너무 긴급한 상황이어서 자원 정리도 하지 않고 최대한 빨리 스레드를 종료해야하여 해당 스레드를 다시 인터럽트 상태로 변경하는 경우도 있음
** 참고)
- 스레드가 과거에 제공했던 stop()이라는 메서드도 호출할 수 있는데, 이것을 사용하면 IDE가 빨간줄로 사용하지 말라고 알려줌
- 이 메서드는 스레드를 바로 중단 시키도록 동작하도록 제공되었었는데 이는 예측 불가능한 문제가 발생할 수 있어서 호출해도 동작하지 않고 더이상 사용되지 않음
2. 프린터 예제
1) 예제 작성
(1) 예제 설명
- 사용자의 입력을 프린터에 출력하는 예제
- 사용자의 입력을 받는 main스레드와 사용자의 입력을 출력하는 printer 스레드로 나누어짐
(2) MyPrinterV1
- volatile: 여러 스레드가 동시에 접근하는 변수에는 volatile 키워드를 붙여주어야 안전하므로 main스레드, printer 스레드 둘다 work 변수에 동시에 접근할 수 있으므로 사용하였음
- ConcurrentLinkedQueue: 여러 스레드가 동시에 접근하는 경우 컬렉션 프레임워크가 제공하는 일반적인 자료 구조를 사용하면 안전하지 않으므로 동시성을 지원하는 동시성 컬렉션을 사용해야함, Queue의 경우 ConcurrentLinkedQueue를 사용하면 됨
- 작성한 코드를 실행해보면 main 스레드에서 값을 입력받고 printer스레드에서 값을 출력하는 것을 확인할 수 있음
** 참고
- volatile, ConcurrentLinkedQueue에 대한 자세한 내용은 뒤에서 다룸
package thread.control.printer;
public class MyPrinterV1 {
public static void main(String[] args) {
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "printer");
printerThread.start();
Scanner scanner = new Scanner(System.in);
while (true) {
log("프린터할 문서를 입력하세요. 종료(q): ");
String input = scanner.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.add(input);
}
}
}
/* 실행 결과
19:56:50.333 [ main] 프린터할 문서를 입력하세요. 종료(q):
a
19:56:51.230 [ main] 프린터할 문서를 입력하세요. 종료(q):
19:56:51.230 [ printer] 출력 시작: a, 대기 문서: []
b
19:56:51.543 [ main] 프린터할 문서를 입력하세요. 종료(q):
c
19:56:51.797 [ main] 프린터할 문서를 입력하세요. 종료(q):
d
19:56:52.067 [ main] 프린터할 문서를 입력하세요. 종료(q):
e
19:56:52.355 [ main] 프린터할 문서를 입력하세요. 종료(q):
19:56:54.234 [ printer] 출력 완료: a
19:56:54.234 [ printer] 출력 시작: b, 대기 문서: [c, d, e]
19:56:57.239 [ printer] 출력 완료: b
19:56:57.240 [ printer] 출력 시작: c, 대기 문서: [d, e]
19:57:00.245 [ printer] 출력 완료: c
19:57:00.245 [ printer] 출력 시작: d, 대기 문서: [e]
19:57:03.250 [ printer] 출력 완료: d
19:57:03.251 [ printer] 출력 시작: e, 대기 문서: []
19:57:06.256 [ printer] 출력 완료: e
q
19:57:14.554 [ printer] 프린터 종료
*/
(2) 동작 설명
- 프린터 동작
- main스레드: 사용자의 입력을 받아서 Printer 인스턴스의 jobQueue에 담음
- printer 스레드: jobQueue가 있는지 확인하고 jobQueue에 내용이 있으면 poll()을 이용해서 꺼낸다음 출력하고 비어있다면 continue를 사용해서 while문을 반복하여 jobQueue에 출력할 내용이 들어올 때 까지 계속 확인함
- 출력하는데 약 3초의 시간이 걸린다고 가정하기 위해 sleep(3000)을 사용하였고, 출력을 완료하면 while문을 다시 반복함
- 프린터 종료
- main스레드: 사용자가 q를 입력하면 printer.work의 값을 false로 변경하고 main 스레드는 while문을 빠져나가고 main 스레드가 종료됨
- printer 스레드: while문에서 work의 값이 false인 것을 확인한 후 while문을 빠져나가고 "프린터 종료"를 출력한 뒤에 printer 스레드가 종료됨
- 이 방식의 문제는 앞서 경험했듯이 종료(q)를 입력했을 때 printer스레드가 반복문을 빠져나오려면 while문을 체크해야 하는데 printer 스레드가 sleep(3000)을 통해 대기 상태에 빠져서 작동하지 않기 때문에 바로 반응하지 않는다는 점임
- 최악의 경우 q를 입력하고 3초 이후에 프린터가 종료되므로 인터럽트를 사용해서 느린 문제를 해결할 수 있음
2) 인터럽트 도입
(1) MyPrinterV2
- Printer의 run() 메서드에서 직접 만든 sleep()이 아닌 Thread.sleep()을 사용하도록 변경하고 main 스레드에서 work 변수를 false로 변경하고 printer스레드의 인터럽트도 함께 호출하면 인터럽트 상태가 되어 catch문으로 넘어가고 break문을 통해서 반복문이 종료됨
- 프로그램을 실행 후 q를 입력하면 즉시 종료되는 것을 확인할 수 있음
package thread.control.printer;
public class MyPrinterV2 {
public static void main(String[] args) {
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "printer");
printerThread.start();
Scanner scanner = new Scanner(System.in);
while (true) {
log("프린터할 문서를 입력하세요. 종료(q): ");
String input = scanner.nextLine();
if (input.equals("q")) {
printer.work = false;
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 (work) {
if (jobQueue.isEmpty()) {
continue;
}
try {
String job = jobQueue.poll();
log("출력 시작: " + job + ", 대기 문서: " + jobQueue);
Thread.sleep(3000);
log("출력 완료: " + job);
} catch (InterruptedException e) {
log("인터럽트!");
break;
}
}
log("프린터 종료");
}
public void addJob(String input) {
jobQueue.add(input);
}
}
}
/* 실행 결과
19:58:05.966 [ main] 프린터할 문서를 입력하세요. 종료(q):
a
19:58:06.602 [ main] 프린터할 문서를 입력하세요. 종료(q):
19:58:06.603 [ printer] 출력 시작: a, 대기 문서: []
b
19:58:06.829 [ main] 프린터할 문서를 입력하세요. 종료(q):
c
19:58:07.053 [ main] 프린터할 문서를 입력하세요. 종료(q):
d
19:58:07.323 [ main] 프린터할 문서를 입력하세요. 종료(q):
e
19:58:07.699 [ main] 프린터할 문서를 입력하세요. 종료(q):
q
19:58:07.997 [ printer] 인터럽트!
19:58:07.998 [ printer] 프린터 종료
*/
3) 인터럽트 코드 개선
(1) MyPrinterV3
- Thread.interrupted() 메서드를 while문의 조건으로 사용하여 기존의 work 변수와 관련된 코드를 모두 제거할 수 있음
- printer 스레드의 인터럽트 상태가 true이면, while의 조건은 false가되고 인터럽트의 상태도 false로 복귀하게 됨
- 물론 프로그램을 실행 후 q를 입력하면 바로 종료됨
package thread.control.printer;
public class MyPrinterV3 {
public static void main(String[] args) {
// ... 기존 코드 동일 생략
if (input.equals("q")) {
printerThread.interrupt();
break;
}
// ... 기존 코드 동일 생략
static class Printer implements Runnable {
// ... 기존 코드 동일 생략
while (!Thread.interrupted()) {
// ... 기존 코드 동일 생략
}
}
3. yield - 양보하기
1) yield
(1) YieldMain
- 1000개의 스레드를 실행하며 각 스레드는 0 ~ 9까지 출력하면 종료되는 단순한 로직임
- run() 메서드에 있는 sleep(1)과 Thread.yield()의 주석 상태를 변경하면서 실행하면서 실행 결과를 관찰
- Empty: sleep(1), Thread.yield() 없이 호출하면 운영체제의 스레드 스케줄링을 따름
- sleep(1): 특정 스레드를 잠시 쉬게 함
- yield(): 다른 스레드에 실행을 양보함
package thread.control.yield;
public class YieldMain {
static final int THREAD_COUNT = 1000;
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
// 1. empty
// sleep(1); // 2. sleep
// Thread.yield();
}
}
}
}
(2) Empty 실행 결과
- 특정 스레드가 쭉 수행된 다음에 다음 스레드가 수행되는 것을 확인할 수 있음
- 참고로 실행 환경에 따라 결과는 달라질 수 있지만 다른 예시보다 상대적으로 하나의 스레드가 쭉 연달아서 실행되다가 다른 스레드로 넘어가는 것을 확인할 수 있음
- 운영체제의 스케줄링 정책과 환경에 따라 다르지만 대략 0.01초(10ms)정도 하나의 스레드가 실행되고 다른 스레드로 넘어감
/* 실행 결과 일부
...
Thread-996 - 7
Thread-996 - 8
Thread-996 - 9
Thread-995 - 8
Thread-995 - 9
Thread-998 - 0
Thread-998 - 1
Thread-998 - 2
Thread-998 - 3
Thread-998 - 4
Thread-998 - 5
Thread-998 - 6
Thread-998 - 7
Thread-998 - 8
Thread-998 - 9
Thread-999 - 0
Thread-999 - 1
Thread-999 - 2
Thread-999 - 3
Thread-999 - 4
Thread-999 - 5
Thread-999 - 6
Thread-999 - 7
Thread-999 - 8
Thread-999 - 9
*/
(3) sleep(1) 실행 결과
- 실행 결과를 보면 거의 대부분의 스레드가 번갈아가면서 실행되고 있는 것을 확인할 수 있음
- sleep(1)을 사용해서 스레드의 상태를 1밀리초 동안 아주 잠깐 RUNNABLE -> TIMED_WAITING으로 변경하기 때문에 스레드는 CPU자원을 사용하지 않고 실행 스케줄링에서 잠시 제외됨
- 1밀리초의 대기 시간 이후 다시 TIMED_WAITING -> RUNNABLE 상태가 되면서 실행 스케줄링에 포함됨
- 결과적으로 TIMED_WAITING 상태가 되면서 다른 스레드에 실행을 양보하게 되고 스케줄링 큐에 대기 중인 다른 스레드가 CPU의 실행 기회를 빨리 얻을 수 있음
- 그러나 이 방식은 RUNNABLE -> TIMED_WAITING -> RUNNABLE로 변경되는 복잡한 과정을 거치고 특정 시간만큼 스레드가 실행되지 않는 단점이 존재함
- 만약 양보할 스레드가 없다면 계속 실행중인 스레드를 더 실행하는 것이 더 나은 선택일 수 있은데 이 방법은 나머지 스레드가 모두 대기 상태로 쉬고 있어도 내 스레드까지 강제로 잠깐 실행되지 않는 것임
- 즉, 양보할 사람이 없는데 혼자서 양보하고 있는 이상한 상황이 될 수 있음
/* 실행 결과 일부
...
Thread-833 - 7
Thread-887 - 8
Thread-886 - 9
Thread-773 - 9
Thread-777 - 9
Thread-961 - 8
Thread-715 - 8
Thread-641 - 9
Thread-887 - 9
Thread-833 - 8
Thread-961 - 9
Thread-715 - 9
Thread-833 - 9
*/
(4) yield() 실행 결과
- 실행 결과를 보면 sleep(1) 처럼은 아니지만 한 스레드가 실행하다가 다른 스레드로 교체되는 것을 확인할 수 있음
- 자바의 스레드가 RUNNABLE상태일 때 운영체제의 스케줄링은 다음과 같은 상태를 가짐
- 실행 상태(Running): 스레드가 CPU에서 실제로 실행 중
- 실행 대기 상태(Ready): 스레드가 실행될 준비가 되었지만 CPU가 바빠서 스케줄링 큐에서 대기 중
- 운영체제는 실행 상태의 스레드를 잠깐만 실행하고 실행 대기 상태로 만든 후 실행 대기 상태의 스레드들을 잠깐 실행 상태로 변경해서 실행하는 과정을 반복함
- 참고로 자바에서는 두 상태를 구분할 수 없음
- Thread.yield(): 현재 실행 중인 스레드가 자발적으로 CPU를 양보하여 다른 스레드가 실행될 수 있도록함
- yield() 메서드를 호출한 스레드는 RUNNABLE상태를 유지하면서 CPU를 양보하므로 다시 스케줄링 큐에 들어가면서 다른 스레드에게 CPU 사용 기회를 넘김
- 자바에서 Thread.yield() 메서드를 호출하면 현재 실행 중인 스레드가 CPU를 양보하도록 힌트를 주어 스레드가 자신에게 할당된 실행 시간을 포기하고 다른 스레드에게 실행 기회를 줌
- 하지만 운영체제의 스케줄러에게 단지 힌트를 제공할 뿐 강제적인 실행 순서를 지정하지않기 때문에 반드시 다른 스레드가 실행되는 것은 아니며 RUNNABLE 상태를 유지하기 때문에 양보할 스레드가 없다면 본인 스레드가 계속 실행될 수 있음
/* 실행 결과 일부
...
Thread-908 - 7
Thread-908 - 8
Thread-908 - 9
Thread-915 - 8
Thread-915 - 9
Thread-913 - 8
Thread-913 - 9
Thread-403 - 8
Thread-403 - 9
Thread-920 - 9
Thread-930 - 8
Thread-930 - 9
Thread-656 - 6
Thread-952 - 9
Thread-969 - 9
Thread-129 - 9
Thread-656 - 7
Thread-487 - 9
Thread-560 - 8
Thread-560 - 9
Thread-656 - 8
Thread-656 - 9
*/
** 참고
- 최근에는 10코어 이상의 CPU도 많기 때문에 스레드 10개 정도만 만들어서 실행하면 양보를 해도 CPU 코어가 남아서 양보하지 않고 계속 수행될 수 있으므로 양보가 크게 없어짐
- CPU 코어 수 이상의 스레드를 만들어야 양보하는 상황을 확인할 수 있으므로 이번 예제에서도 1000개의 스레드를 생성하여 실행한 것임
- 직접 만든 log()는 현재 시간도 획득해야하고, 날짜 포멧도 지정해야하는 등 복잡하기 때문에 이 사이에 스레드의 컨텍스트 스위칭이 발생하기 쉽기 때문에 스레드의 실행 순서를 일정하게 출력하기 어려워 log()를 사용하지 않고 System.out.println()을 사용하였음
4. 프린터 예제에 yield 도입
1) 예제에 yield 도입
(1) MyPrinterV4 - yield도입
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
continue;
}
- 앞서 개발한 프린터 예제를 보면 yield()를 적용하기 딱 좋은 곳이 있는데 바로 위의 부분임
- 이 코드는 인터럽트가 발생하기 전까지 계속 인터럽트의 상태를 체크하고 jobQueue의 상태를 확인하는데 문제는 쉴 틈 없이 CPU에서 이 로직이 계속 반복해서 수행되고 있다는 점임
- 1초에 while문을 수억 번 반복할 수도 있어 결과적으로 CPU 자원을 많이 사용하게 됨
- 현재 작동하는 스레드가 아주 많다고 가정할 때 인터럽트도 걸리지 않고, jobQueue도 비어있는데 이런 체크 로직에 CPU 자원을 많이 사용하게 되면 정작 필요한 스레드들의 효율이 상대적으로 떨어질 수 있음
- 차라리 그 시간에 다른 스레드들을 더 많이 실행해서 jobQueue에 필요한 작업을 빠르게 만들어서 넣어주는게 더 효율적이므로 jobQueue에 작업이 비어있으면 yield()를 호출해서 다른 스레드에 작업을 양보하는게 전체적인 관점에서 보면 더 효율적임
package thread.control.printer;
public class MyPrinterV3 {
public static void main(String[] args) {
// ... 기존 코드 동일 생략
}
static class Printer implements Runnable {
Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
@Override
public void run() {
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); // 추가
continue;
}
// ... 기존 코드 동일 생략
}
** 참고
- 이 부분은 선택이지만 yield()를 사용하는 것도 CPU자원을 계속 쓰기 때문에 sleep()을 1 ~ 10 정도 넣어주면 CPU 사용률을 조금 더 줄일 수 있음
728x90