스레드 제어와 생명 주기(1)
#Java/멀티스레드
정리
스레드 기본 정보
Thread 클래스는 스레드를 생성하고 관리하는 기능을 제공한다. Thread 클래스가 제공하는 정보들을 직접 확인할 수 있다.
0. 스레드 조회
Thread mainThread = Thread.currentThread();
현재 실행중인 스레드 객체를 가져온다.
1. 스레드 생성
스레드를 생성할 때는 실행할 Runnable 인터페이스의 구현체와, 스레드의 이름을 전달할 수 있다.
Thread myThread = new Thread(new HelloRunnable(), "myThread");
- Runnable 인터페이스: 실행할 작업을 포함하는 인터페이스이다.
HelloRunnable클래스는Runnable인터페이스를 구현한 클래스이다. - 스레드 이름:
"myThread"라는 이름으로 스레드를 생성한다. 이 이름은 디버깅이나 로깅 목적으로 유용하다. 참고로 이름을 생략하면Thread-0,Thread-1과 같은 임의의 이름이 생성된다.
2. 스레드 객체 정보
log("myThread = " + myThread);
myThread객체를 문자열로 변환하여 출력한다.Thread클래스의toString()메서드는 스레드 ID, 스레드 이름, 우선순위, 스레드 그룹을 포함하는 문자열을 반환한다.Thread[#21,myThread,5,main]
3. 스레드 ID
log("myThread.threadId() = " + myThread.threadId());
- threadId(): 스레드의 고유 식별자를 반환하는 메서드이다. 이 ID는 JVM 내에서 각 스레드에 대해 유일하다. ID는 스레드가 생성될 때 할당되며, 직접 지정할 수 없다.
4. 스레드 이름
log("myThread.getName() = " + myThread.getName());
- getName(): 스레드의 이름을 반환하는 메서드이다. 생성자에서
"myThread"라는 이름을 지정했기 때문에, 이 값이 반환된다. 참고로 스레드 ID는 중복되지 않지만, 스레드 이름은 중복될 수 있다.
5. 스레드 우선순위
log("myThread.getPriority() = " + myThread.getPriority());
- getPriority(): 스레드의 우선순위를 반환하는 메서드이다. 우선순위는 1 (가장 낮음)에서 10 (가장 높음)까지의 값으로 설정할 수 있으며, 기본값은 5이다.
setPriority()메서드를 사용해서 우선순위를 변경할 수 있다.- 우선순위가 높을수록 더 많이 실행된다. 운영체제 스케줄러에게 힌트를 주는거다.
- 근데 운영체제가 알잘딱으로 해주기 때문에, 직접 우선순위를 지정할 일은 거의 없다.
- 우선순위는 스레드 스케줄러가 어떤 스레드를 우선 실행할지 결정하는 데 사용된다. 하지만 실제 실행 순서는 JVM 구현과 운영체제에 따라 달라질 수 있다.
- CPU가 2개이고 스레드가 2개이면 우선순위가 크게 상관이 없지만, CPU가 적고, Thread가 엄청 많으면 우선순위가 높은 스레드가 더 자주 실행된다.
6. 스레드 그룹
log("myThread.getThreadGroup() = " + myThread.getThreadGroup());
- getThreadGroup(): 스레드가 속한 스레드 그룹을 반환하는 메서드이다. 스레드 그룹은 스레드를 그룹화하여 관리할 수 있는 기능을 제공한다. 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속하게 된다.
- 스레드 그룹은 여러 스레드를 하나의 그룹으로 묶어서 특정 작업(예: 일괄 종료, 우선순위 설정 등)을 수행할 수 있다.
- 부모 스레드(Parent Thread): 새로운 스레드를 생성하는 스레드를 의미한다. 스레드는 기본적으로 다른 스레드에 의해 생성된다. 이러한 생성 관계에서 새로 생성된 스레드는 생성한 스레드를 부모로 간주한다. 예를 들어
myThread는main스레드에 의해 생성되었으므로main스레드가 부모 스레드이다. main스레드는 기본으로 제공되는main스레드 그룹에 소속되어 있다. 따라서myThread도 부모 스레드인main스레드의 그룹인main스레드 그룹에 소속된다.- 스레드 그룹 기능은 직접적으로 잘 사용하지는 않는다.
7. 스레드 상태
log("myThread.getState() = " + myThread.getState());
- getState(): 스레드의 현재 상태를 반환하는 메서드이다. 반환되는 값은
Thread.State열거형에 정의된 상수 중 하나이다. 주요 상태는 다음과 같다.- NEW: 스레드가 아직 시작되지 않은 상태이다. (생성만 한 상태)
- 아직
thread.start()를 호출하지 않는 상태이다.
- 아직
- RUNNABLE: 스레드가 실행 중이거나 실행될 준비가 된 상태이다.
- BLOCKED: 스레드가 동기화 락을 기다리는 상태이다.
- WAITING: 스레드가 다른 스레드의 특정 작업이 완료되기를 기다리는 상태이다. (대기)
- TIMED_WAITING: 일정 시간 동안 기다리는 상태이다.
- Sleep 을 생각해보자. Sleep 스레드 getState()하면 이 상태이다.
- TERMINATED: 스레드가 실행을 마친 상태이다.
- NEW: 스레드가 아직 시작되지 않은 상태이다. (생성만 한 상태)
스레드 생명주기
자바 스레드(Thread)의 생명 주기는 여러 상태(state)로 나뉘어지며, 각 상태는 스레드가 실행되고 종료되기까지의 과정을 나타낸다.
- New (새로운 상태)
- 스레드가 생성되고 아직 시작되지 않은 상태이다.
- 이 상태에서는
Thread객체가 생성되지만,start()메서드가 호출되지 않은 상태이다. - 예:
Thread thread = new Thread(runnable);
- Runnable (실행 가능 상태)
- 스레드가 실행될 준비가 된 상태이다. 이 상태에서 스레드는 실제로 CPU에서 실행될 수 있다.
start()메서드가 호출되면 스레드는 이 상태로 들어간다.- 예:
thread.start(); - 이 상태는 스레드가 실행될 준비가 되어 있음을 나타내며, 실제로 CPU에서 실행될 수 있는 상태이다. 그러나 Runnable 상태에 있는 모든 스레드가 동시에 실행되는 것은 아니다. 운영체제의 스케줄러가 각 스레드에 CPU 시간을 할당하여 실행하기 때문에, Runnable 상태에 있는 스레드는 스케줄러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행된다.
- CPU 1개에 스레드 A.start(), 스레드 B.start()하면 사람이 봤을 때는 동시에 실행되는 것처럼 보이고, 두 개 다 실행되고 있다. 물리적으로 보면, CPU 입장에서는 CPU가 처리하고 있는 스레드가 있고, OS 스케줄러에 의해 대기중인 스레드가 있다. 근데 둘 다 실행 가능한 상태이므로, Runnable 상태라고 부르는거다.
- 참고로 운영체제 스케줄러의 실행 대기열에 있든, CPU에서 실제 실행되고 있든 모두
RUNNABLE상태이다. 자바에서 둘을 구분해서 확인할 수는 없다. - 보통 실행 상태라고 부른다.
- Blocked, Wating, Timed Wating의 경우 CPU 실행 스케줄러에 들어가지 않으므로 Runnable이 아니다.
- Blocked (차단 상태)
- 스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태이다.
- 예를 들어,
synchronized블록에 진입하기 위해 락을 얻어야 하는 경우 이 상태에 들어간다. - 예:
synchronized (lock) { ... }코드 블록에 진입하려고 할 때, 다른 스레드가 이미lock의 락을 가지고 있는 경우. - 지금은 이런 상태가 있다 정도만 알아두자. 이 부분은 뒤에서 자세히 다룬다.
- Waiting (대기 상태)
- 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
wait(),join()메서드가 호출될 때 이 상태가 된다.- 스레드는 다른 스레드가
notify()또는notifyAll()메서드를 호출하거나,join()이 완료될 때까지 기다린다. - 예:
object.wait(); - 지금은 이런 상태가 있다 정도만 알아두자. 이 부분은 뒤에서 자세히 다룬다.
- Timed Waiting (시간 제한 대기 상태)
- 스레드가 특정 시간 동안 다른 스레드의 작업이 완료되기를 기다리는 상태이다.
sleep(long millis),wait(long timeout),join(long millis)메서드가 호출될 때 이 상태가 된다.- 주어진 시간이 경과하거나 다른 스레드가 해당 스레드를 깨우면 이 상태에서 벗어난다.
- 예:
Thread.sleep(1000); - 지금은 이런 상태가 있다 정도만 알아두자. 이 부분은 뒤에서 자세히 다룬다.
- Terminated (종료 상태)
- 스레드의 실행이 완료된 상태이다.
- run까지 스택 프레임에서 나가버린 상태.
- 스레드가 정상적으로 종료되거나, 예외가 발생하여 종료된 경우 이 상태로 들어간다.
- 스레드는 한 번 종료되면 다시 시작할 수 없다.
- 스레드의 실행이 완료된 상태이다.
자바 스레드의 상태 전이 과정
- New → Runnable:
start()메서드를 호출하면 스레드가Runnable상태로 전이된다. - Runnable → Blocked/Waiting/Timed Waiting: 스레드가 락을 얻지 못하거나,
wait()또는sleep()메서드를 호출할 때 해당 상태로 전이된다. - Blocked/Waiting/Timed Waiting → Runnable: 스레드가 락을 얻거나, 기다림이 완료되면 다시
Runnable상태로 돌아간다. - Runnable → Terminated: 스레드의
run()메서드가 완료되면 스레드는Terminated상태가 된다.
package thread.control;
import static util.MyLogger.log;
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 {
@Override
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);
}
}
}
}
Thread.sleep(): 해당 코드를 호출한 스레드는TIMED_WAITING상태가 되면서 특정 시간 만큼 대기한다.
- state5 = TERMINATED
myThread가run()메서드를 실행 종료하고 나면TERMINATED상태가 된다.myThread입장에서run()이 스택에 남은 마지막 메서드인데,run()까지 실행되고 나면 스택이 완전히 비워진다. 이렇게 스택이 비워지면 해당 스택을 사용하는 스레드도 종료된다.
Runnable - 체크 예외
Runnable 인터페이스는 다음과 같이 정의되어 있다.
public interface Runnable {
void run();
}
Runnable 인터페이스의 run() 메서드는 아무런 체크 예외를 던지지 않는다.
따라서 Runnable 인터페이스의 run() 메서드를 재정의 하는 곳에서는 체크 예외를 밖으로 던질 수 없다.
복습: 체크 예외 재정의 규칙
- 자식 클래스에 재정의된 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만을 던질 수 있다.
- 원래 메서드가 체크 예외를 던지지 않는 경우, 재정의된 메서드도 체크 예외를 던질 수 없다.
안전한 예외 처리
체크 예외를 run() 메서드에서 던질 수 없도록 강제함으로써, 개발자는 반드시 체크 예외를 try-catch 블록 내에서 처리하게 된다. 이는 예외 발생 시 예외가 적절히 처리되지 않아서 프로그램이 비정상 종료되는 상황을 방지할 수 있다. 특히 멀티스레딩 환경에서는 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지할 수 있다.
하지만 체크 예외는 최근에 선호하지 않는 방식이다. 체크 예외를 런타임 예외로 변경하는 간단한 유틸리티를 만들어 사용하자.
public abstract class ThreadUtils {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
log("인터럽트 발생, " + e.getMessage());
throw new RuntimeException(e);
}
}
}
join
join() 메서드를 통해 WAITING (대기 상태)가 무엇이고 왜 필요한지 알아보자.
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("작업 완료");
}
}
}
여기서 thread-1, thread-2 가 종료된 다음에 main 스레드를 가장 마지막에 종료하려면 어떻게 해야할까?
(ex. thread-1에서는 1 ~ 50을 더하고, thread-2에서는 51 ~ 100을 더하고, 최종적으로 main에서는 1 ~ 100을 더한 결과를 출력)
핵심은 main 스레드가 thread-1, thread-2 의 계산이 끝날 때까지 기다려야 한다는 점이다.
특정 스레드를 기다리게 하는 가장 간단한 방법은 sleep() 을 사용하는 것이다.
하지만 이렇게 sleep() 을 사용해서 무작정 기다리는 방법은 대기 시간에 손해도 있고, 또 thread-1, thread-2 의 수행시간이 달라지는 경우에는 정확한 타이밍을 맞추기 어렵다.
더 나은 방법은 thread-1, thread-2 가 계산을 끝내고 종료될 때까지 main 스레드가 기다리는 방법이다.
예를 들어서 main 스레드가 반복문을 사용해서 thread-1, thread-2 의 상태가 TERMINATED 가 될 때까지 계속 확인하는 방법이 있다.
while(thread.getState() != TERMINATED) {
// 스레드의 상태가 종료될 때 까지 계속 반복
}
// 계산 결과 출력
이런 방법은 번거롭고 또 계속되는 반복문은 CPU 연산을 사용한다. 이때 join() 메서드를 사용하면 깔끔하게 문제를 해결할 수 있다.
package thread.control.join;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
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);
}
}
}
(this: this 는 호출된 인스턴스 메서드가 소속된 객체를 가리키는 참조이며, 이것이 스택 프레임 내부에 저장되어 있다. 컨텍스트 스위칭이 발생할 때 인스턴스 필드에 계속해서 접근할 수 있는 이유이다.)
main 스레드에서 다음 코드를 실행하게 되면 main 스레드는 thread-1, thread-2 가 종료될 때 까지 기다린다. 이때 main 스레드는 WAITING 상태가 된다.
Waiting (대기 상태) : join() 을 호출하는 스레드는 대상 스레드가 TERMINATED 상태가 될 때 까지 대기한다. 대상 스레드가 TERMINATED 상태가 되면 호출 스레드는 다시 RUNNABLE 상태가 되면서 다음 코드를 수행한다.
이렇듯 특정 스레드가 완료될 때 까지 기다려야 하는 상황이라면 join() 을 사용하면 된다.
하지만 join() 의 단점은 다른 스레드가 완료될 때 까지 무기한 기다리는 단점이 있다. 만약 다른 스레드의 작업을 일정 시간 동안만 기다리고 싶다면 어떻게 해야할까?
join - 특정 시간 만큼만 대기
join() 은 두 가지 메서드가 있다.
join(): 호출 스레드는 대상 스레드가 완료될 때 까지 무한정 대기한다.join(ms): 호출 스레드는 특정 시간 만큼만 대기한다. 호출 스레드는 지정한 시간이 지나면 다시RUNNABLE상태가 되면서 다음 코드를 수행한다.
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);
}
WAITING, TIMED_WAITING, join()정리
다른 스레드가 끝날 때 까지 무한정 기다려야 한다면 join() 을 사용하고, 다른 스레드의 작업을 무한정 기다릴 수 없다면 join(ms) 를 사용하면 된다. 물론 기다리다 중간에 나오는 상황인데, 결과가 없다면 추가적인 오류 처리가 필요할 수 있다.
'Java > 멀티스레드&동시성' 카테고리의 다른 글
| [Java] 38. 동기화 - synchronized (0) | 2025.07.01 |
|---|---|
| [Java] 37. 메모리 가시성 (0) | 2025.07.01 |
| [Java] 36. 스레드 제어와 생명 주기(2) (0) | 2025.07.01 |
| [Java] 34. 스레드 생성과 실행 (0) | 2025.07.01 |
| [Java] 33. 프로세스와 스레드 (0) | 2025.07.01 |