일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch5
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch6
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch12
- 게시글 목록 api
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch1
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch11
- 스프링 mvc1 - 스프링 mvc
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch9
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch4
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch13
- 스프링 mvc1 - 서블릿
- 자바 중급2편 - 컬렉션 프레임워크
- 자바의 정석 기초편 ch2
- 2024 정보처리기사 수제비 실기
- @Aspect
- 스프링 입문(무료)
- 스프링 고급 - 스프링 aop
- 자바 중급1편 - 날짜와 시간
- 스프링 mvc2 - 검증
- Today
- Total
나구리의 개발공부기록
스레드 생성과 실행, 스레드 시작, 데몬 스레드, 스레드 생성-Runnable, 로거 만들기, 여러 스레드 만들기, Runnable을 만드는 다양한 방법 본문
스레드 생성과 실행, 스레드 시작, 데몬 스레드, 스레드 생성-Runnable, 로거 만들기, 여러 스레드 만들기, Runnable을 만드는 다양한 방법
소소한나구리 2025. 2. 9. 23:59출처 : 인프런 - 김영한의 실전 자바 - 고급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
프로젝트 환경 구성
1. IDE: 인텔리제이
2. Name: java-adv1
3. Build system: IntelliJ
4. JDK: 21이상(자바 19이상에서 제공하는 기능을 사용함)
1. 스레드 시작
1) 스레드 시작
(1) HelloThread
- Thread 클래스를 상속하고 스레드가 실행할 코드를 run() 메서드에 재정의
- Thread.currentThread()를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있음
- Thread.currentThread().getName(): 실행 중인 스레드의 이름을 조회함
package thread.start;
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
(2) HelloThreadMain
- 앞서 만든 HelloThread 스레드 객체를 생성하고 start() 메서드를 호출
- start() 메서드는 스레드를 실행하는 아주 특별한 메서드로 helloThread.start()를 호출하면 HelloThread가 run()메서드를 실행함
- 실행 결과는 스레드의 실행 순서에 따라 다르게 출력될 수 있음
** 주의
- run() 메서드를 호출하는 것이 아니라 반드시 start() 메서드를 호출해야 스레드에서 run() 코드가 실행됨
package thread.start;
public class HelloThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
HelloThread helloThread = new HelloThread();
System.out.println(Thread.currentThread().getName() + ": start() 호출 전");
helloThread.start();
System.out.println(Thread.currentThread().getName() + ": start() 호출 후");
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
/* 실행 결과
main: main() start
main: start() 호출 전
main: start() 호출 후
Thread-0: run()
main: main() end
*/
(3-1) 스레드 생성 전
- 실행 결과를 보면 main() 메서드는 main이라는 이름의 스레드가 실행하는 것을 확인할 수 있음
- 프로세스가 최소한 하나는 있어야 코드를 실행할 수 있으므로 자바는 실행 시점에 main이라는 이름의 스레드를 만들고 프로그램의 시작점인 main() 메서드를 실행함
(3-2) 스레드 생성 후
- HelloThread 스레드 객체를 생성한 다음에 start() 메서드를 호출하면 자바는 스레드를 위한 별도의 스택 공간을 할당함
- 스레드 객체를 생성하고 반드시 start()를 호출해야 스택 공간을 할당 받고 스레드가 작동함
- 스레드에 이름을 주지 않으면 자바는 스레드에 Thread-0, Thread-1처럼 임의의 이름을 부여함
- 새로운 Thread-0스레드가 사용할 전용 스택 공간이 마련되고 Thread-0 스레드는 run() 메서드의 스택 프레임을 스택에 올리면서 run() 메서드를 시작함
(3-3) 메서드를 실행하면 스택 위에 스택 프레임이 쌓임
- main 스레드는 main() 메서드의 스택 프레임을 스택에 올리면서 시작하고 직접 만드는 스레드는 run() 메서드의 스택 프레임을 스택에 올리면서 시작함
(4) 시간의 흐름으로 분석
- main 스레드가 HelloThread 인스턴스를 생성하고 이때 스레드에 이름을 부여하지 않으면 자바가 Thread-0, Thread-1과 같은 임의의 이름을 부여함
- start() 메서드를 호출하면 Thread-0 스레드가 시작되면서 Thread-0 스레드가 run()메서드를 호출함
- 여기서 핵심은 main 스레드가 run()메서드를 실행하는 것이 아니라 Thread-0 스레드가 run() 메서드를 실행한다는 점임
- main 스레드는 단지 start() 메서드를 통해 Thread-0 스레드에게 실행을 지시할 뿐이며 main 스레드는 다른 스레드에게 일을 시작하라고 지시만 하고 바로 start() 메서드를 빠져나옴
- 이제 main스레드와 Thread-0 스레드는 동시에 실행됨
- main 스레드의 입장에서 보면 그림의 1, 2, 3번 코드를 멈추지 않고 계속 수행하며 run() 메서드는 main이 아닌 별도의 스레드에서 실행됨
(5) 스레드 간 실행 순서는 보장하지 않음
- 스레드는 동시에 실행되기 때문에 스레드 간에 실행 순서는 얼마든지 달라질 수 있으므로 다양한 실행 결과가 나올 수 있음
- CPU 코어가 2개여서 물리적으로 정말 동시에 실행될 수도 있고 하나의 CPU 코어에 시간을 나누어 실행 될 수도 있음
- 그리고 한 스레드가 얼마나 오랜기간 실행되는지도 보장하지 않으므로 한 스레드가 먼저 다 수행된 다음에 다른 스레드가 수행될 수도 있고 둘이 완전히 번갈아 가면서 수행되는 경우도 있음
- 즉, 스레드는 순서와 실행 기간을 모두 보장하지 않으며 이것이 바로 멀티 스레드임
2) start() vs run()
(1) BadThreadMain
- 스레드의 start() 대신에 재정의한 run() 메서드를 직접 호출하면 HelloThread가 아니라 main이 run()메서드를 호출하는 것을 알 수 있음
- 자바를 처음 실행하면 main 스레드가 main() 메서드를 호출하면서 시작하고 main 스레드는 HelloThread 인스턴스에 있는 run() 이라는 메서드를 호출함
- main 스레드가 run() 메서드를 실행했기 때문에 main 스레드가 사용하는 스택위에 run() 스택 프레임이 올라가며 결과적으로 main스레드에서 모든 것을 처리한 것이됨
- 스레드의 start() 메서드는 스레드에 스택 공간을 할당하면서 스레드를 시작하는 아주 특별한 메서드임
- main 스레드가 아닌 별도의 스레드에서 재정의한 run() 메서드를 실행하려면 반드시 start() 메서드를 호출해야 함
package thread.start;
public class BadThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
HelloThread helloThread = new HelloThread();
System.out.println(Thread.currentThread().getName() + ": start() 호출 전");
helloThread.run(); // run() 직접 실행
System.out.println(Thread.currentThread().getName() + ": start() 호출 후");
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
/* 실행 결과
main: main() start
main: start() 호출 전
main: run()
main: start() 호출 후
main: main() end
*/
2. 데몬 스레드
1) 데몬 스레드
(1) 사용자 스레드(non-daemon 스레드)
- 프로그램의 주요 작업을 수행함
- 작업이 완료될 때까지 실행되며 모든 user 스레드가 종료되면 JVM도 종료됨
(2) 데몬 스레드
- 백그라운드에서 보조적인 작업을 수행함
- 모든 user 스레드가 종료되면 데몬 스레드는 자동으로 종료됨
- JVM은 데몬 스레드의 실행 완료를 기다리지 않고 종료되며 데몬 스레드가 아닌 모든 스레드가 종료되면 자바 프로그램도 종료됨
** 용어 - 데몬
- 그리스 신화에서 데몬은 신과 인간 사이의 중간적 존재로 보이지 않게 활동하며 일상적인 일들을 도왔음
- 이런 의미로 컴퓨터 과학에서는 사용자에게 직접적으로 보이지 않으면서 시스템의 백그라운드에서 작업을 수행하는 것을 데몬 스레드, 데몬 프로세스라고 함
- 예를 들어 사용하지 않는 파일이나 메모리를 정리하는 작업들이 있음
(3) DaemonThreadMain
- static 내부 클래스로 Thread를 상속 받아서 쓰레드를 생성하고 run() 메서드를 재정의할 때 10초간 기다렸다가 다음 메서드가 실행되도록 작성
- setDaemon(true): 데몬 스레드로 설정, 데몬 스레드 여부는 start() 실행 전에 결정해야하며 이후에는 변경되지 않으며 해당 메서드를 호출하지않으면 기본적으로 user 스레드로 동작함
- 실행 결과를 보면 Thread-0이 데몬 스레드로 설정되고 출력되자 마자 유일한 user스레드인 main 스레드가 종료되면서 자바 프로그램이 종료되어 run() end가 출력되지 않는 것을 확인할 수 있음
** 참고
- run() 메서드 안에 Thread.sleep()을 호출할 때 체크 예외인 InterruptedException을 밖으로 던질 수 없고 반드시 잡아야함
- run() 메서드는 체크 예외를 밖을 던질 수 없는데 이부분은 뒤에서 설명함
package thread.start;
public class DaemonThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
DaemonThread daemonThread = new DaemonThread();
daemonThread.setDaemon(true);
daemonThread.start();
System.out.println(Thread.currentThread().getName() + ": main() end");
}
static class DaemonThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run() start");
try {
Thread.sleep(10000); // 10초간 실행
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ": run() end");
}
}
}
/* 실행 결과
main: main() start
main: main() end
Thread-0: run() start
*/
(2) setDaemon(false)
- 만약 setDaemon(false)로 설정하거나 해당 메서드를 주석처리하고 호출해보면 main 스레드가 종료되어도 user 스레드인 Thread-0종료될 때까지 자바 프로그램이 종료되지 않는 것을 확인할 수 있음
- Thread-0: run() end가 출력되고 user스레드인 main스레드와 Thread-0스레드가 모두 종료되어야 자바 프로그램이 종료됨
3. 스레드 생성-Runnable
1) Runnable
(1) HelloRunnable
- 스레드를 만들 때는 Thread 클래스를 상속 받는 방법과 Runnable 인터페이스를 구현하는 방법이 있는데 실무에서는 주로 Runnable을 구현하는 방법을 사용함
- Runnable인터페이스를 구현하면 무조건 run() 메서드를 재정의해야함
package thread.start;
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
(2) HelloRunnableMain
- start()로 스레드를 실행하는 것과 실행 결과는 기존의 Thread로 동작할 때와 동일함
- 차이가 있다면 스레드와 해당 스레드가 실행할 작업이 서로 분리되어있다는 점으로 스레드 객체를 생성할 때 실행할 작업을 생성자로 전달하면 됨
package thread.start;
public class HelloRunnableMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
HelloRunnable helloRunnable = new HelloRunnable();
Thread thread = new Thread(helloRunnable);
thread.start();
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
/* 실행 결과
main: main() start
main: main() end
Thread-0: run()
*/
2) Thread 상속 vs Runnable 구현
(1) Thread 클래스 상속받는 방식
- 장점
- 간단한 구현: Thread 클래스를 상속 받아 run() 메서드만 재정의하면 됨
- 단점
- 상속의 제한: 자바는 단일 상속만을 허용하므로 이미 다른 클래스를 상속받고 있는 경우 Thread 클래스를 상속 받을 수 없음
- 유연성 부족: 인터페이스를 사용하는 방법에 비해 유연성이 떨어짐
(2) Runnable 인터페이스를 구현하는 방식
- 장점
- 상속의 자유로움: Runnable 인터페이스 방식은 다른 클래스를 상속받아도 문제없이 구현할 수 있음
- 코드의 분리: 스레드와 실행할 작업을 분리하여 코드의 가독성을 높일 수 있음
- 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있음
- 단점
- Runnable 객체를 생성하고 이를 Thread에 전달하는 과정이 추가되어 코드가 약간 복잡해짐
(3) 정리
- 스레드를 사용할 때는 Thread를 상속 받는 방법보다 Runnable 인터페이스를 구현하는 방식을 사용해야 함
- 스레드와 실행할 작업을 명확히 분리하고 인터페이스를 사용하므로 Thread 클래스를 직접 상속하는 방식보다 더 유연하고 유지보수 하기 쉬운 코드를 만들 수 있음
4. 로거 만들기
1) 로거 만들기
(1) MyLogger
- 지금은 어떤 스레드가 코드를 실행하는지 출력해보기 위해서 상당히 긴 출력문을 매번 작성해야하는데, 현재 시간, 스레드 이름, 출력 내용등을 편리하게 한번에 확인할 수 있도록 하는 로거를 작성
- 프로젝트 전반에 사용되는 유틸리티이므로 util이라는 새로운 패키지에 작성
- DateTimeFormatter: 현재 시간을 원하는 포맷으로 출력하기 위함
- printf:
- %s는 문자열을 뜻하며 인자를 순서대로 사용함
- 여기서는 현재 시간, 스레드 이름, 출력할 객체를 순서대로 사용하였음
- 출력할 객체의 타입을 Object으로 한 이유는 %s를 사용하면 toString()을 사용해서 문자열로 변환 후 출력해주기 때문에 문자열 뿐만 아니라 객체도 출력할 수 있음
- %9s는 문자를 출력할 때 9칸을 확보한다는 뜻으로 9칸이 전부 채워지지 않으면 왼쪽에 그만큼 비워둠(오른쪽 정렬 효과)
package util;
public abstract class MyLogger {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
public static void log(Object obj) {
String time = LocalTime.now().format(formatter);
System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), obj);
}
}
(2) MyLoggerMain
- MyLogger.log()를 사용해보면 스레드 이름과 출력시간, 출력할 객체가 깔끔하게 출력되는 것을 확인할 수 있음
- 스태틱임포트를 사용하면 메서드 이름만으로 간단히 사용할 수 있음
- 스레드를 학습할 때는 스레드 이름, 그리고 해당 스레드가 언제 실행되었는지 확인하는 것이 중요하므로 스레드강의에서는System.out.println() 대신 MyLogger를 사용하여 진행
- 실무에서는 로그 라이브러리를 사용함
package util;
import static util.MyLogger.*;
public class MyLoggerMain {
public static void main(String[] args) {
log("hello thread");
log(123);
}
}
/* 출력 결과
21:16:54.806 [ main] hello thread
21:16:54.807 [ main] 123
*/
5. 여러 스레드 만들기
(1) ManyThreadMainV1
- 스레드 3개를 생성할 때 모두 같은 HelloRunnable 인스턴스를 스레드의 실행 작업으로 전달했으므로 Thread-0, Thread-1,Thread-2는 모두 HelloRunnable 인스턴스에 있는 run() 메서드를 실행함
- 스레드의 실행 순서는 보장되지 않으므로 실행결과는 실행할 때마다 달라짐
package thread.start;
public class ManyThreadMainV1 {
public static void main(String[] args) {
log("main() start");
HelloRunnable helloRunnable = new HelloRunnable();
Thread thread1 = new Thread(helloRunnable);
thread1.start();
Thread thread2 = new Thread(helloRunnable);
thread2.start();
Thread thread3 = new Thread(helloRunnable);
thread3.start();
log("main() end");
}
}
/* 실행 결과
21:21:42.139 [ main] main() start
21:21:42.142 [ main] main() end
Thread-0: run()
Thread-2: run()
Thread-1: run()
*/
(2) ManyThreadMainV2
- 반복문을 활용하여 스레드를 유동적으로 여러개 생성할 수 있음
- 실행해보면 마찬가지로 스레드의 실행순서가 보장되지 않는 것을 확인할 수 있음
package thread.start;
public class ManyThreadMainV2 {
public static void main(String[] args) {
log("main() start");
HelloRunnable helloRunnable = new HelloRunnable();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(helloRunnable);
thread.start();
}
log("main() end");
}
}
6. Runnable을 만드는 다양한 방법
1) Runnable을 만드는 다양한 방법
(1) InnerRunnableMainV1 - 정적 중첩 클래스 사용
- 특정 클래스 안에서만 사용되는 경우 중첩 클래스를 사용하면 됨
package thread.start;
public class InnerRunnableMainV1 {
public static void main(String[] args) {
log("main() start");
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
log("main() end");
}
static class MyRunnable implements Runnable {
@Override
public void run() {
log("run()");
}
}
}
(2) 익명 클래스 사용
- 특정 메서드 안에서만 간단히 정의하고 사용하고 싶다면 익명 클래스를 사용하면 됨
package thread.start;
public class InnerRunnableMainV2 {
public static void main(String[] args) {
log("main() start");
Runnable runnable = new Runnable() {
@Override
public void run() {
log("run()");
}
};
Thread thread = new Thread(runnable);
thread.start();
log("main() end");
}
}
(3) 익명 클래스 변수 없이 직접 전달
- 익명 클래스를 참조하는 변수를 만들지 않고 직접 전달할 수 있음
package thread.start;
public class InnerRunnableMainV3 {
public static void main(String[] args) {
log("main() start");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log("run()");
}
});
thread.start();
log("main() end");
}
}
(4) 람다
- 람다를 사용하면 메서드(함수) 코드 조각을 전달할 수 있음
- 아직 람다를 학습하지 않았기 때문에 강의에서는 정적 중첩 클래스나 익명 클래스를 주로 사용할 예정
package thread.start;
public class InnerRunnableMainV4 {
public static void main(String[] args) {
log("main() start");
Thread thread = new Thread(() -> log("run()"));
thread.start();
log("main() end");
}
}
7. 문제와 풀이
1) Thread 상속
(1) 요구사항
- 다음 요구 사항에 맞게 멀티 스레드 프로그램을 작성
- 1. Thread 클래스를 상속받은 CounterThread라는 스레드 클래스를 생성
- 2. 이 스레드는 1부터 5까지의 숫자를 1초 간격으로 출력해야하며 출력은 log() 기능을 사용
- 3. 실행 결과를 참고하여 main() 메서드에서 CounterThread 스레드 클래스를 만들고 실행
package thread.start.test;
import static util.MyLogger.log;
public class StartTest1Main {
// 여기에 코드 작성
}
실행 결과
09:46:23.329 [ Thread-0] value: 1
09:46:24.332 [ Thread-0] value: 2
09:46:25.338 [ Thread-0] value: 3
09:46:26.343 [ Thread-0] value: 4
09:46:27.349 [ Thread-0] value: 5
(2) 정답
package thread.start.test;
public class StartTest1Main {
public static void main(String[] args) {
new CounterThread().start();
}
static class CounterThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
log("value: " + i);
sleep(1000);
}
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2) Runnable 구현
(1) 요구사항
- 다음 요구 사항에 맞게 멀티 스레드 프로그램을 작성
- 1. CounterRunnable 이라는 이름의 클래스를 만들고 Runnable 인터페이스를 구현
- 2. CounterRunnable는 1부터 5까지의 숫자를 1초 간격으로 출력해야하며 출력은 log() 기능을 사용
- 3. 실행 결과를 참고하여 main() 메서드에서 CounterRunnable의 인스턴스를 이용하여 Thread를 생성하고 실행
- 4. 스레드의 이름은 counter로 지정해야 함
package thread.start.test;
import static util.MyLogger.log;
public class StartTest2Main {
// 여기에 코드 작성
}
실행 결과
09:53:36.705 [ counter] value: 1
09:53:37.713 [ counter] value: 2
09:53:38.719 [ counter] value: 3
09:53:39.725 [ counter] value: 4
09:53:40.726 [ counter] value: 5
(2) 정답
package thread.start.test;
import static util.MyLogger.log;
public class StartTest2Main {
public static void main(String[] args) {
CounterRunnable counterRunnable = new CounterRunnable();
Thread counter = new Thread(counterRunnable, "counter");
counter.start();
}
static class CounterRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
log("value: " + i);
sleep(1000);
}
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
3) Runnable 익명 클래스 구현
(1) 요구사항
- 2번의 문제2를 익명 클래스로 구현
(2) 정답
package thread.start.test;
import static util.MyLogger.log;
public class StartTest3Main {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
log("value: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}, "counter").start();
}
}
4) 여러 스레드 사용
(1) 요구사항
- Thread-A, Thread-B 두 스레드를 만들고 Thread-A는 1초에 한 번씩 "A"를 출력하고 Thread-B는 0.5초에 한 번씩 "B"를 출력함
- 이 프로그램은 강제 종료할 때까지 계속 실행되어야 함
package thread.start.test;
import static util.MyLogger.log;
public class StartTest4Main {
// 여기에 코드 작성
}
실행 결과
10:04:27.000 [ Thread-A] A
10:04:27.000 [ Thread-B] B
10:04:27.507 [ Thread-B] B
10:04:28.006 [ Thread-A] A
10:04:28.012 [ Thread-B] B
10:04:28.518 [ Thread-B] B
10:04:29.011 [ Thread-A] A
10:04:29.023 [ Thread-B] B
... 무한 실행
(2) 정답
package thread.start.test;
import static util.MyLogger.log;
public class StartTest4Main {
public static void main(String[] args) {
new Thread(new PrintRunnable("A", 1000), "Thread-A").start();
new Thread(new PrintRunnable("B", 500), "Thread-B").start();
}
static class PrintRunnable implements Runnable {
private String content;
private int ms;
public PrintRunnable(String content, int ms) {
this.content = content;
this.ms = ms;
}
@Override
public void run() {
while (true) {
log(content);
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}