Thread 클래스란?
main 메서드도 내부적으로 main이라는 이름의 스레드에서 실행스레드 생성 방법
Thread myThread = new Thread(new HelloRunnable(), "myThread");
HelloRunnable은 Runnable 인터페이스를 구현한 클래스"myThread")을 지정하면 로깅이나 디버깅 시 유용주요 메서드 및 정보
| 메서드 | 설명 |
|---|---|
threadId() | 스레드 고유 ID 반환 (JVM 내에서 유일) |
getName() | 스레드 이름 반환(이름 중복 가능) |
getPriority() | 스레드 우선순위 (1~10, 기본값: 5, 가장 낮음: 1 가장 높음: 10) |
getThreadGroup() | 소속된 스레드 그룹 (기본적으로 부모 스레드와 동일) |
getState() | 스레드의 현재 상태 확인 (NEW, RUNNABLE 등) |
스레드 상태 예시
mainThread.getState() → RUNNABLE (실행 중)myThread.getState() → NEW (start 호출 전)스레드 주요 메서드 예제
package thread.control;
import static util.MyLogger.log;
import thread.start.HelloRunnable;
public class ThreadInfoMain {
public static void main(String[] args) {
// main 스레드
Thread mainThread = Thread.currentThread();
log("mainTread = " + mainThread);
log("mainThread.threadId() = " + mainThread.threadId());
log("mainThread.getName() = " + mainThread.getName());
log("mainThread.getPriority() = " + mainThread.getPriority()); // 1 ~ 10(기본값: 5)
log("mainThread.getThreadGroup() = " + mainThread.getThreadGroup());
log("mainThread.getState() = " + mainThread.getState());
// myThread 스레드
Thread myThread = new Thread(new HelloRunnable(), "myThread");
log("myThread = " + myThread);
log("myThread.threadId() = " + myThread.threadId());
log("myThread.getName() = " + myThread.getName());
log("myThread.getPriority() = " + myThread.getPriority()); // 1 ~ 10(기본값: 5)
log("myThread.getThreadGroup() = " + myThread.getThreadGroup());
log("myThread.getState() = " + myThread.getState());
}
}
//main 스레드 출력
09:55:58.709 [ main] mainThread = Thread[#1,main,5,main]
09:55:58.713 [ main] mainThread.threadId() = 1
09:55:58.713 [ main] mainThread.getName() = main
09:55:58.716 [ main] mainThread.getPriority() = 5
09:55:58.716 [ main] mainThread.getThreadGroup() = java.lang.ThreadGroup[name=main,maxpri=10]
09:55:58.716 [ main] mainThread.getState() = RUNNABLE
//myThread 출력
09:55:58.717 [ main] myThread = Thread[#21,myThread,5,main]
09:55:58.717 [ main] myThread.threadId() = 21
09:55:58.717 [ main] myThread.getName() = myThread
09:55:58.717 [ main] myThread.getPriority() = 5
09:55:58.717 [ main] myThread.getThreadGroup() = java.lang.ThreadGroup[name=main,maxpri=10]
09:55:58.717 [ main] myThread.getState() = NEW
Thread 클래스의 toString() 메서드는 스레드 ID, 이름, 우선순위, 그룹을 포함하는 문자열을 반환합니다.
자바 스레드는 실행 중 상태 변화에 따라 여러 상태(State)를 가지며, 이는 Thread.State 열거형(enum)으로 정의되어 있습니다.

스레드의 주요 상태
| 상태 | 설명 |
|---|---|
| NEW | Thread 객체 생성 후 start() 호출 전 상태 |
| RUNNABLE | 실행 가능 상태 (실제로 실행 중이거나 CPU 할당 대기 중) |
| BLOCKED | 락(lock)을 기다리는 중 (ex: synchronized 블록 진입 대기) |
| WAITING | 무기한 대기 (ex: join(), wait() 사용 시) |
| TIMED_WAITING | 일정 시간 대기 (ex: sleep(1000), join(1000)) |
| TERMINATED | 실행 완료 또는 예외 발생 후 종료된 상태 |
💡 일시 중지 상태(BLOCKED, WAITING, TIMED_WAITING)를 통틀어 Suspended States라고 부르기도 하나, 이는 자바 공식 용어는 아니고 설명용이다.
상태 전이 흐름
NEW → RUNNABLE: start() 호출RUNNABLE → BLOCKED / WAITING / TIMED_WAITING: 락을 기다리거나 wait(), sleep() 등 호출BLOCKED / WAITING / TIMED_WAITING → RUNNABLE: 락 획득 또는 시간 만료, notify() 등으로 깨어남RUNNABLE → TERMINATED: run() 종료예제: 상태 확인 코드
Thread thread = new Thread(new MyRunnable(), "myThread");
log(thread.getState()); // NEW
thread.start();
Thread.sleep(1000);
log(thread.getState()); // TIMED_WAITING 또는 RUNNABLE
Thread.sleep(4000);
log(thread.getState()); // TERMINATED
이 코드를 통해 스레드가 생성부터 종료까지 어떤 상태를 거치는지 실행 로그로 확인할 수 있습니다.
join() 메서드 - 스레드 간 기다림 (대기)join() 메서드를 통해 WAITING(대기 상태)가 무엇이고 왜 필요한지 학습합니다. 먼저 스레드로 특정 작업을 수행하는 간단한 예제를 하나 만들어봅니다.
package thread.control.join;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
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");
}
public static class Job implements Runnable {
@Override
public void run() {
log("작업 시작");
sleep(2000);
log("작업 종료");
}
}
}
15:13:40.734 [ main] Start
15:13:40.736 [ main] End
15:13:40.736 [ thread-2] 작업 시작
15:13:40.736 [ thread-1] 작업 시작
15:13:42.741 [ thread-1] 작업 완료
15:13:42.741 [ thread-2] 작업 완료
스레드의 실행 순서는 보장되지 않기 때문에 실행 결과는 약간 다를 수 있습니다. 그런데 만약 thread-1, thread-2가 종료된 다음에 main 스레드를 가장 마지막 종료하려면 어떻게 해야 할까요? 예를 들어 main 스레드가 thread-1, thread-2에 각각 어떤 작업을 지시하고, 그 결과를 받아서 처리하고 싶다면 어떻게 해야 할까요?
package thread.control.join;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
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);
}
}
}
15:36:28.347 [ main] Start
15:36:28.349 [ thread-1] 작업 시작
15:36:28.349 [ thread-2] 작업 시작
15:36:28.352 [ main] task1.result = 0
15:36:28.352 [ main] task2.result = 0
15:36:28.352 [ main] task1 + task2 = 0
15:36:28.352 [ main] End
15:36:30.355 [ thread-1] 작업 완료 result=1275
15:36:30.355 [ thread-2] 작업 완료 result=3775
join()은 다른 스레드가 종료될 때까지 현재 스레드를 대기시키는 메서드입니다.
스레드 간 동기화가 필요한 경우 매우 유용합니다.
join() 기본 동작
thread.join();
thread가 종료될 때까지 무기한 대기합니다.WAITING 상태가 됩니다.시간 제한 대기
thread.join(1000); // 최대 1초 동안만 대기
thread가 먼저 끝나면 대기 종료TIMED_WAITING 상태대기가 왜 필요한가?
예시: 병렬 계산
Thread t1 = new Thread(...); // 1~50 합산
Thread t2 = new Thread(...); // 51~100 합산
t1.start();
t2.start();
t1.join(); // t1이 끝날 때까지 대기
t2.join(); // t2가 끝날 때까지 대기
int result = t1.result + t2.result;
만약 join() 없이 결과를 바로 출력하면, 두 스레드가 계산을 마치기도 전에 결과를 읽어 0이 나올 수 있습니다.
잘못된 방식: sleep()으로 기다리기
Thread.sleep(3000); // 예상 종료 시간만큼 기다림
main 스레드가 너무 빨리 진행해버림join()보다 신뢰성 떨어짐예제 실행 흐름 비교
| 방법 | 결과 |
|---|---|
sleep() 사용 | 결과가 0일 수도 있음 (계산 안 끝남) |
join() 사용 | 정확히 계산 끝난 후 결과 확인 가능 |
join(1000) 사용 | 1초만 기다리므로 결과가 없을 수도 있음 |
sleep() 유틸리티Thread.sleep()은 왜 예외를 던질까?
Thread.sleep()은 체크 예외인 InterruptedException을 던집니다.sleep()을 사용할 때는 반드시 try-catch로 감싸야 합니다.try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Runnable.run()에서는 체크 예외를 던질 수 없다
@Override
public void run() throws InterruptedException { // ❌ 컴파일 에러
Thread.sleep(1000);
}
Runnable 인터페이스의 run() 메서드는 체크 예외를 던지지 않도록 선언되어 있스빈다run() 안에서는 무조건 try-catch로 처리해야 합니다.왜 이런 제약이 있는가?
자바의 체크 예외 설계 원칙 때문입니다.
run()을 실행하는 쪽에서는 InterruptedException을 처리할 준비가 안 돼 있을 수 있으므로, 강제로 예외 처리를 유도한다.유틸리티로 처리 단순화: ThreadUtils.sleep()
예외 처리를 반복하지 않기 위해 다음처럼 유틸리티를 만들 수 있습니다.
public abstract class ThreadUtils {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
import static util.ThreadUtils.sleep;
void run() {
sleep(3000); // 예외 처리 없이 깔끔하게 사용
}
Thread.sleep()은 체크 예외 처리 때문에 불편합니다.Runnable.run()은 체크 예외를 던질 수 없기 때문에 try-catch 필수입니다.sleep() 유틸리티를 사용합니다.join() 예제 문제 & 실행 시간 비교문제 1: join()을 순차적으로 사용한 경우
t1.start();
t1.join(); // t1 종료까지 대기
t2.start();
t2.join(); // t2 종료까지 대기
t3.start();
t3.join(); // t3 종료까지 대기
[t1] 1, 2, 3
[t2] 1, 2, 3
[t3] 1, 2, 3
모든 스레드 실행 완료
3초 * 3스레드)문제 2: join()을 병렬로 사용하는 경우
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
[t1] 1, 2, 3
[t2] 1, 2, 3
[t3] 1, 2, 3
모든 스레드 실행 완료
main 스레드는 모든 스레드 종료까지 대기핵심 포인트 비교
| 방식 | 설명 | 총 실행 시간 |
|---|---|---|
순차 join() | 스레드 종료 후 다음 스레드 시작 | 약 9초 |
병렬 join() | 모두 동시에 시작, 종료까지 기다림 | 약 3초 |
이렇게 join() 사용 위치에 따라 전체 성능과 동작 방식이 크게 달라집니다. 마지막으로, join(ms)을 이용해 시간 제한 대기도 가능한 점도 있습니다!
참고: this의 비밀
어떤 메서드를 호출하는 것은, 정확히 말하자면 특정 스레드가 어떤 메서드를 호출하는 것입니다. 스레드는 메서드의 호출을 관리하기 위해 메서드 단위로 스택 프레임을 만들고 해당 스택 프레임을 스택 위에 쌓아 올립니다. 이때 인스턴스의 메서드를 호출하면, 어떤 인스턴스의 메서드를 호출했는지 기억하기 위해, 해당 인스턴스의 참조값을 스택 프레임 내부에 저장해둡니다. 이것이 바로 자주 사용하던 this입니다.
특정 메서드 안에서 this를 호출하면 바로 스택프레임 안에 있는 this 값을 불러서 사용하게 됩니다. 즉, 자신의 인스턴스를 구분해서 this를 사용할 수 있는 것 입니다. 예를 들어서 필드에 접근할 때 this를 생략하면 자동으로 this를 참고해서 필드에 접근합니다. 정리하면 this는 호출된 인스턴스 메서드가 소속된 객체를 가리키는 참조이며, 이것이 스택 프레임 내부에 저장되어 있습니다.