// 실행 결과
11:40:31.503 [ main] myThread.state1 = NEW
11:40:31.505 [ main] myThread.start()
11:40:31.505 [ myThread] start
11:40:31.505 [ myThread] myThread.state2 = RUNNABLE
11:40:31.505 [ myThread] sleep() start
11:40:32.507 [ main] myThread.state3 = TIMED_WAITING
11:40:34.510 [ myThread] sleep() end
11:40:34.512 [ myThread] myThread.state4 = RUNNABLE
11:40:34.512 [ myThread] end
11:40:36.511 [ main] myThread.state5 = TERMINATED
11:40:36.512 [ main] end
Thread.currentThread() 를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.
Thread.sleep(): 해당 코드를 호출한 스레드는 TIMED_WAITING 상태가 되면서 특정 시간 만큼 대기한다. 시간은 밀리초(ms) 단위이다. 1밀리초 = 1/1000 초, 1000밀리초 = 1초이다.
Thread.sleep(): InterruptedException 이라는 체크 예외를 던진다. 따라서 체크 예외를 잡아서 처리하거나 던져야 한다
InterruptedException: 은 인터럽트가 걸릴 때 발생.
3.3. 체크 예외 재정의
public interface Runnable {
void run();
}
Runnable 인터페이스는 위와 같이 정의 되어있다.
체크 예외는:
부모 메서드가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없다.
자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.
위 규칙에 따라, runnable 인터페이스의 run() 메서드를 재정의하는 곳에서는 체크 예외를 밖으로 던질 수 없음.
체크 예외를 run() 메서드에서 던질 수 없게 강제하고 있는데, 이는 개발자가 반드시 예외를 try-cahtch 블록 내에서 처리해야된다. (프로그램의 비정상 종료 상황 방지). 특히 멀티스레딩 환경에서는 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지할 수 있음.
3.4. Sleep 유틸리티
Thread.sleep() 메서드는 InterruptedException 체크 예외를 발생시킨다. 이를 매번 try-catch로 감싸주지 않는 방식으로 변경할 수 있음.
package util;
import static util.MyLogger.log;
public abstract class ThreadUtils {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
log("인터럽트 발생, " + e.getMessage());
throw new RuntimeException(e);
}
}
}
3.5. join
Join은 특정 시간 만큼만 대기함.
join(): 호출 스레드는 대상 스레드가 완료될 때까지 무한정 대기
join(ms): 호출 스레드는 특정 시간 동안 만큼만 대기. 호출 스레드는 지정한 시간이 지나면 다시 RUNNABLE 상태가됨.
package thread.control.join;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class JoinMainV4 {
public static void main(String[] args) throws InterruptedException {
log("Start");
SumTask task1 = new SumTask(1, 50);
Thread thread1 = new Thread(task1, "thread-1");
thread1.start();
//스레드가 종료될 때 까지 대기
log("join(1000) - main 스레드가 thread1 종료까지 1초 대기");
thread1.join(1000);
log("main 스레드 대기 완료");
log("task1.result = " + task1.result);
}
static class SumTask implements Runnable {
int startValue;
int endValue;
int result = 0;
public SumTask(int startValue, int endValue) {
this.startValue = startValue;
this.endValue = endValue;
}
@Override
public void run() {
log("작업 시작");
sleep(2000);
int sum = 0;
for (int i = startValue; i <= endValue; i++) {
sum += i;
}
result = sum;
log("작업 완료 result = " + result);
}
}
}
별도의 스레드에서 1-50까지 더하고, 그 결과를 조회
join(1000)을 사용해서 1초만 대기
// 실행 결과
17:34:54.572 [ main] Start
17:34:54.575 [ main] join(1000) - main 스레드가 thread1 종료까지 1초 대기
17:34:54.575 [ thread-1] 작업 시작
17:34:55.580 [ main] main 스레드 대기 완료
17:34:55.585 [ main] task1.result = 0
17:34:56.580 [ thread-1] 작업 완료 result = 1275
main 스레드는 join(1000)을 사용해서 thread-1을 1초간 기다린다. 이때 main 스레드의 생태는 TIMED_WAITING이다.
thread-1의 작업에는 2초가 걸린다.
1초가 지나도 thread-1의 작업이 완료되지 않으므로, main 스레드는 대기를 중단. 그리고 main 스레드는 다시 RUNNABLE 상태로 바뀌면서 다음 코드를 수행. 이때 thread-1의 작업이 아직 완료되지 않았기 때문에 task1.result=0이 출력.
main 스레드가 종료된 이후에 thread-1이 계산을 끝낸다. result=1275가 출력.
package thread.control.test;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class JoinTest1Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyTask(), "t1");
Thread t2 = new Thread(new MyTask(), "t2");
Thread t3 = new Thread(new MyTask(), "t3");
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
System.out.println("모든 스레드 실행 완료");
}
static class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
log(i);
sleep(1000);
}
}
}
}
위 코드는 총 9초의 실행이 걸림. join()이 될때까지 3초를 기다림
package thread.start;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class JoinTest2Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyTask(), "t1");
Thread t2 = new Thread(new MyTask(), "t2");
Thread t3 = new Thread(new MyTask(), "t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("모든 스레드 실행 완료");
}
static class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
log(i);
sleep(1000);
}
}
}
}
위 코드는 효율적으로 스레드를 사용하고 있다.
총 걸리는 시간은 3초.
4. 스레드 제어와 생명 주기2
4.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=false");
task.runFlag = false;
}
static class MyTask implements Runnable {
volatile boolean runFlag = true; // 제어 변수
@Override
public void run() {
while (runFlag) {
log("작업 중");
sleep(3000);
}
log("자원 정리");
log("작업 종료");
}
}
}
// 실행 결과
14:58:27.520 [ work] 작업 중
14:58:30.525 [ work] 작업 중
14:58:31.510 [ main] 작업 중단 지시 runFlag=false
14:58:33.532 [ work] 자원 정리 <- 2초
14:58:33.533 [ work] 작업 종료
특정 스레드의 작업을 중단하는 가장 쉬운 방법은 변수를 사용. 하지만, 변수를 사용하더라도 스레드가 즉각 반응을 하지 않음.
sleep(3000)으로 인해 3초동안 잠들어 있기때문에, 변수가 변경되어도 자원 정리까지 대기 시간 발생
이를 해결하기 위해 인터럽트를 사용할 수 있다. 인터럽트를 사용하면 WAITING, TIMED_WAITING같은 대기 상태의 스레드를 직접 꺠워서, RUNNABLE 상태로 만들 수 있음.
특정 스레드의 인스턴스에 interrupt() 메서드가 호출되면, 해당 스레드에 인터럽트가 발생.
인터럽트가 발생하면 해당 스레드에 InterruptedException 발생. 이로 인해 RUNNABLE 상태로 전환.
다만, 즉각적으로 InterruptedException이 발생하지는 않는다.
// 실행 결과
18:10:40.024 [ work] 작업 중
18:10:43.026 [ work] 작업 중
18:10:44.011 [ main] 작업 중단 지시 thread.interrupt()
18:10:44.021 [ main] work 스레드 인터럽트 상태1 = true
18:10:44.021 [ work] work 스레드 인터럽트 상태2 = false
18:10:44.022 [ work] interrupt message=sleep interrupted
18:10:44.022 [ work] state=RUNNABLE
18:10:44.022 [ work] 자원 정리
18:10:44.023 [ work] 작업 종료
Thread.interrupt를 통해 작업을 중단하면 거의 즉각적으로 인터럽트가 발생
이떄 work 스레드는 TIMED_WAITING -> RUNNABLE 상태로 변경.
하지만, while (true) 부분을 체크하지 않기때문에 인터럽트가 발생해도 이 부분은 항상 true 이기 떄문에 다음 코드로 넘어감. 그리고 sleep()을 호출하고 나서야 인터럽트가 발생.
while( isInterrupted() )으로 변경하면 조금 더 빨리 반응할 수 있음. 하지만 이렇게 하게되면 work 상태가 계속 true로 유지되는게 문제.
이를 해결하기 위해, 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);
log("자원 정리 완료");
} catch (InterruptedException e) {
log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " +
Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
// 실행 결과
...
15:40:45:356 [ work] 작업 중
15:40:45:356 [ work] 작업 중
15:40:45:357 [ work] 작업 중
15:40:45:357 [ work] 작업 중
15:40:45:357 [ main] 작업 중단 지시 - thread.interrupt()
15:40:45:357 [ work] 작업 중
15:40:45:364 [ work] work 스레드 인터럽트 상태2 = false
15:40:45:364 [ main] work 스레드 인터럽트 상태1 = false
15:40:45:364 [ work] 자원 정리 시도
15:40:46:365 [ work] 자원 정리 완료
15:40:46:365 [ work] 작업 종료
결과적으로 while문을 탈출하는 시점에, 스레드의 인터럽트 상태도 false로 변경.
자바는 인터럽트 예외가 한번 발생하면, 스레드의 인터럽트 상태를 다시 정상(false)로 되돌림. 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생.
4.2. Yield
어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다. 그런데 특정 스레드가 크게 바쁘지 않은 상황 이어서 다른 스레드에 CPU 실행 기회를 양보하고 싶을 수 있다. 이렇게 양보하면 스케줄링 큐에 대기 중인 다른 스레드 가 CPU 실행 기회를 더 빨리 얻을 수 있다
자바의 스레드가 RUNNABLE 상태일때, 운영체제의 스케줄링은 아래의 상태를 가질 수 있다:
Running: CPU에서 실행중
Ready: CPU에서 실행되길 기다리며 큐에 대기중
yield()는 현재 실행 중인 스레드가 자발적으로 CPU를 양보하여 다른 스레드가 실행될 수 있도록한다. 메서드를 호출한 스레드는 RUNNABLE 상태를 유지하면서 CPU를 양보한다. 즉, 이 스레드는 다시 스케줄링 큐에 들어가면서 다른 스레드에게 CPU 사용 기회를 넘긴다. 다만, CPU가 비어있다면 큐에서 다시 꺼내서 실행된다.
package thread.control.printer;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;
import static util.MyLogger.log;
public class MyPrinterV4 {
public static void main(String[] args) throws InterruptedException {
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "printer");
printerThread.start();
Scanner userInput = new Scanner(System.in);
while (true) {
System.out.println("프린터할 문서를 입력하세요. 종료 (q): ");
String input = userInput.nextLine();
if (input.equals("q")) {
printerThread.interrupt();
break;
}
printer.addJob(input);
}
}
static class Printer implements Runnable {
Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
@Override
public void run() {
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); //추가
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.offer(input);
}
}
}
// 입력과 실행 결과
프린터할 문서를 입력하세요. 종료 (q):
a
프린터할 문서를 입력하세요. 종료 (q):
15:46:30:670 [ printer] 출력 시작: a, 대기 문서: []
b
프린터할 문서를 입력하세요. 종료 (q):
c
프린터할 문서를 입력하세요. 종료 (q):
d
프린터할 문서를 입력하세요. 종료 (q):
15:46:33:675 [ printer] 출력 완료: a
15:46:33:675 [ printer] 출력 시작: b, 대기 문서: [c, d]
15:46:36:690 [ printer] 출력 완료: b
15:46:36:690 [ printer] 출력 시작: c, 대기 문서: [d]
q
15:46:37:343 [ printer] 인터럽트!
15:46:37:343 [ printer] 프린터 종료
Process finished with exit code 0