
⬛ 데몬 스레드
사용자 스레드(non-daemon 스레드)
- 프로그램의 주요 작업을 수행한다. 작업이 완료될 때까지 실행된다.
- 모든 user 스레드가 종료되면 JVM도 종료된다.
데몬 스레드
- 백그라운드에서 보조적인 작업을 수행한다.모든 user 스레드가 종료되면 데몬 스레드는 자동으로 종료된다.
- JVM은 데몬 스레드의 실행 완료를 기다리지 않고 종료된다. 데몬 스레드가 아닌 모든 스레드가 종료되면, 자바 프로그램도 종료된다.
- setDaemon(true)로 설정 가능하다.
⬛ 스레드 로거
public 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);
}
}
⬛ 스레드 기본 정보
아래와 같이 스레드 기본 정보를 다룰 수 있다.
public class ThreadInfoMain {
public static void main(String[] args) {
// main 스레드
Thread mainThread = Thread.currentThread();
log("mainThread = " + mainThread);
log("mainThread.threadId() = " + mainThread.getId()); // Id는 JVM 내 각 스레드에 대해 유일
log("mainThread.getName() = " + mainThread.getName());
log("mainThread.getPriority() = " + mainThread.getPriority()); // 1~10(기본값 5)
log("mainThread.getThreadGroup() = " + mainThread.getThreadGroup()); // 모든 스레드는 부모 스레드와 동일한 스레드 그룹
log("mainThread.getState() = " + mainThread.getState());
log("------------------------------------------------");
// myThread 스레드
Thread myThread = new Thread(new HelloRunnable(), "myThread"); // 구현체, 스레드 이름 전달
log("myThread = " + myThread);
log("myThread.threadId() = " + myThread.getId());
log("myThread.getName() = " + myThread.getName());
log("myThread.getPriority() = " + myThread.getPriority());
log("myThread.getThreadGroup() = " + myThread.getThreadGroup());
log("myThread.getState() = " + myThread.getState());
}
private static void log(String s) {
System.out.println(s);
}
}

⬛ 스레드 생명 주기

🟢스레드 상태
- New (새로운 상태) : 스레드가 생성되었으나 아직 시작되지 않은 상태.
- Runnable (실행 가능 상태): start() 메서드 호출 후 상태. 스레드가 실행 중이거나 실행될 준비가 된 상태.
- 일시 중지 상태들 (Suspended States)
- Blocked (차단 상태) : 스레드가 동기화 락을 기다리는 상태.
- Waiting (대기 상태) : 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태.
- Timed Waiting (시간 제한 대기 상태) : 스레드가 일정 시간 동안 다른 스레드의 작업을 기다리는 상태.
- Terminated (종료 상태) : 스레드의 실행이 완료된 상태.
🔶실습 코드
public class ThreadStateMain {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable(), "myThread");
log("myThread.state1 = " + thread.getState()); // NEW
log("myThread.start()");
thread.start();
Thread.sleep(1000);
log("myThread.state3 = " + thread.getState()); // TIMED_WAITING
Thread.sleep(4000);
log("myThread.state5 = " + thread.getState()); // TERMINATED
log("end");
}
static class MyRunnable implements Runnable {
public void run() {
try {
log("start");
log("myThread.state2 = " + Thread.currentThread().getState()); // RUNNABLE
log("sleep() start");
Thread.sleep(3000);
log("sleep() end");
log("myThread.state4 = " + Thread.currentThread().getState()); // RUNNABLE
log("end");
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private static void log(String s) {
System.out.println(s);
}
}

⬛ 체크 예외 재정의
위 코드에서 볼 수 있듯, Thread.sleep()은 InterruptedException이라는 체크 예외를 던진다. 따라서 체크 예외를 잡아서 처리하거나 던져야 한다. 하지만 Runnable의 run() 메서드를 구현할때 InterruptedException 예외를 밖으로 던질 수 없다.
🟢자바 메서드 재정의 규칙
- 체크 예외
- 부모 메서드가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없다.
- 자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.
- 언체크(런타임) 예외
- 예외 처리를 강제하지 않으므로 상관없이 던질 수 있다.
Runnable 인터페이스 run() 메서드는 아무 체크 예외도 던지지 않는다.
public interface Runnable {
void run();
}
따라서, 아래에서 Main에서 체크예외를 밖으로 던질수 있는 반면,
run()은 체크예외를 밖으로 던질 수 없다.
public class CheckedExceptionMain {
public static void main(String[] args) throws Exception {
throw new Exception();
}
static class CheckedRunnable implements Runnable {
@Override
public void run() /*throws Exception*/ { // 주석 풀면 예외 발생
//throw new Exception(); // 주석 풀면 예외 발생
}
}
}
🤔이렇게 규칙을 만든 이유 ??
이는 자바 컴파일러가 예외를 잡게 하도록 하기 위함이며, run()메서드에서 던질 수 없고 반드시 try-catch블록 내 예외 처리를 강제함으로써 스레드 안정성과 일관성을 유지할 수 있다. (또한 이처럼 체크 예외 강제는 자바 초창기 기조이기도 하다.)
🟢Sleep 유틸리티
sleep()은 InterruptedException 체크 예외를 계속 발생시키고, try~catch를 계속 사용하는 것은 번거롭기 때문에,
아래처럼 체크 예외를 런타임 예외로 변경하는 유틸리티를 만들어 사용할 수 있다.
public abstract class ThreadUtils {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
log("인터럽트 발생, " + e.getMessage());
}
}
}
⬛ join - 시작
join()메서드를 통해 WAITING(대기 상태)와 그 필요성에 대해 알아보자.
public class JoinMainV0 {
public static void main(String[] args) {
log("Start");
Thread thread1 = new Thread(new Job(), "thread-1");
Thread thread2 = new Thread(new Job(), "thread-2");
thread1.start();
thread2.start();
log("End");
}
static class Job implements Runnable {
@Override
public void run() {
log("작업 시작");
sleep(2000);
log("작업 완료");
}
}
}

위는 main 스레드가 먼저 종료된다. thread-1,2가 종료 된 후 main 스레드를 가장 마지막에 종료하려면 어떻게할까.
여기서잠깐, 위와 비슷한상황의 예시 코드를 보자.
두개의 스레드로 각각 1~50,51~100까지 더하는 예제를 보자.
public class JoinMainV1 {
public static void main(String[] args) {
log("Start");
SumTask task1 = new SumTask(1, 50);
SumTask task2 = new SumTask(51, 100);
Thread thread1 = new Thread(task1, "thread-1");
Thread thread2 = new Thread(task2, "thread-2");
thread1.start();
thread2.start();
log("task1.result = " + task1.result);
log("task2.result = " + task2.result);
int sumAll = task1.result + task2.result;
log("task1 + task2 = " + sumAll);
log("End");
}
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);
}
}
}

😑최종 합 작업 결과는 0이 출력된다. main 스레드는 두 스레드를 실행하고,자신의 코드를 모두 실행후 종료된다.
따라서 원하는 작업 결과를 얻기 위해선 thread1,2의 계산이 끝날때 까지 기다려야한다...!

🪄참고
this 는 호출된 인스턴스 메서드가 소속된 객체를 가리키는 참조이며, 이것이 스택 프레임 내부에 저장되어 있다.
(이를 통해 각 스레드가 구분된다.)
Sleep()을 사용할 수 도있지만, 이렇게하면 대기시간에 손해도 생기고 정확한 타이밍을 맞추기도 어렵다.
따라서 Join()을 사용한다.그러면 main 스레드는 WAITING 상태가 되고, thread1,2의 종료를 기다린다.
public class JoinMainV3 {
public static void main(String[] args) throws InterruptedException {
log("Start");
SumTask task1 = new SumTask(1, 50);
SumTask task2 = new SumTask(51, 100);
Thread thread1 = new Thread(task1, "thread-1");
Thread thread2 = new Thread(task2, "thread-2");
thread1.start();
thread2.start();
// 스레드가 종료될 때 까지 대기
log("join() - main 스레드가 thread1, thread2 종료까지 대기");
thread1.join();
thread2.join();
log("main 스레드 대기 완료");
log("task1.result = " + task1.result);
log("task2.result = " + task2.result);
int sumAll = task1.result + task2.result;
log("task1 + task2 = " + sumAll);
log("End");
}
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);
}
}
}

🟢join()
- 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
- join() 을 호출하는 스레드는 대상 스레드가 TERMINATED 상태가 될 때 까지 대기한다. 대상 스레드가 TERMINATED 상태가 되면 호출 스레드는 다시 RUNNABLE 상태가 되면서 다음 코드를 수행한다.
- 이렇듯 특정 스레드가 완료될 때 까지 기다려야 하는 상황일때 사용한다.
- join(ms)를 사용하면 특정 시간 만큼만 대기한다.
'Java' 카테고리의 다른 글
| [Java] 김영한의 실전 자바 - 고급 1편 섹션7 고급 동기화 - concurrent.Lock (4) | 2024.08.28 |
|---|---|
| [Java] 김영한의 실전 자바 - 고급 1편 섹션6 동기화 - synchronized (0) | 2024.08.19 |
| [Java의 정석] chapter13 쓰레드 thread 알아보기 (0) | 2024.07.18 |
| [Java] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 섹션1 프로세스와 스레드 소개 (0) | 2024.07.16 |
| [Java] 김영한의 실전 자바 - 중급편2 섹션10 컬렉션 프레임워크 - 순회, 정렬, 전체 정리 (0) | 2024.07.08 |