Thread
프로세스 내에서 실제로 작업을 수행하는 실행 단위로, 모든 프로세스는 최소 하나 이상의 스레드를 가지고 있다.
- 프로세스: 운영체제로부터 자원을 할당받는 작업의 단위(Code, Data, Heap, Stack 영역 독립)
- 스레드: 프로세스가 할당받은 자원을 이용하는 실행 단위(Stack, PC Register만 독립, 나머지 영역은 공유)
스레드 구현
Section titled “스레드 구현”구현 방법으로는 아래 두 개의 방법이 있으며 큰 차이는 없으나 Thread 클래스를 상속 받으면 다른 클래스를 상속 받을 수 없기 때문에 Runnable 방법을 권장한다.
Thread 클래스를 상속받아 구현
Section titled “Thread 클래스를 상속받아 구현”Thread 클래스를 직접 상속받아 run() 메서드를 오버라이딩한다.
public class MyThread extends Thread {
@Override public void run() { // Do something }}Java는 다중 상속을 지원하지 않으므로, 다른 클래스를 상속받을 수 없다는 단점이 있다.
Runnable 인터페이스를 구현
Section titled “Runnable 인터페이스를 구현”Runnable 인터페이스는 run() 메서드 하나만 정의되어 있는 함수형 인터페이스다.
public class MyThread implements Runnable {
@Override public void run() { // Do Something }}다른 클래스를 상속받을 수 있으며, Java 8부터는 람다식으로도 구현할 수 있다.
스레드 실행
Section titled “스레드 실행”스레드를 실행할 때는 run()이 아닌 start() 메서드를 호출해야 한다.
void example() { // Runnable 인터페이스 구현체 실행 Runnable r = new MyRunnable(); Thread t1 = new Thread(r); // 생성자: Thread(Runnable target)
// 람다식 활용 Thread t2 = new Thread(() -> System.out.println("Lambda Thread"));
t1.start(); t2.start();}start()메서드 호출 시, 대기 상태 진입 후 스레드 스케줄러에 의해 실행(바로 실행 X)- 한 번 실행된 스레드는 다시 실행 불가능(두 번 이상 실행 시
IllegalThreadStateException예외 발생)
start() vs run()
Section titled “start() vs run()”run(): 생성된 스레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출start(): 새로운 스레드를 생성하기 위한 준비 작업 수행- 새로운 실행 흐름을 위한 호출 스택(Call Stack) 생성
- OS 스케줄러에게 실행 요청
- 새로 생성된 호출 스택에
run()메서드를 올리고 실행
호출 스택을 강제로 출력하는 코드 사용하여 결과를 확인해보면 start() 메서드를 호출한 경우 main 스레드와 별도의 스레드가 생성되어 실행되는 것을 확인할 수 있다.
스레드 우선순위
Section titled “스레드 우선순위”스레드는 priority라는 속성(멤버변수)를 가지고 있으며 이 속성은 스레드의 우선순위를 나타낸다.
- 스레드의 우선순위는 1 ~ 10 사이의 값을 가짐(기본값 5)
- 우선순위는 JVM이 OS 스케줄러에게 주는 힌트일 뿐이며, 실제 실행 순서를 보장하지 않음
스케줄링 메서드
Section titled “스케줄링 메서드”| 메서드 | 역할 | 설명 |
|---|---|---|
sleep(long millis) | 일시 정지 | 지정된 시간 동안 스레드 일시 정지 상태로 만듬 |
join() | 대기 | 다른 스레드의 작업이 끝날 때까지 대기 |
interrupt() | 깨움 | 일시 정지 상태(sleep, join, wait)인 스레드에게 예외(InterruptedException)를 발생시켜 실행 대기 상태로 만듬 |
yield() | 양보 | 실행 중에 자신에게 주어진 실행 시간을 다른 스레드에게 양보하고 자신은 실행 대기 상태로 변경 |
이 외에 suspend(), resume(), stop()은 교착 상태(Deadlock)를 유발할 가능성이 있어 deprecated 되었다.
스레드의 상태
Section titled “스레드의 상태”| 상태 | 설명 |
|---|---|
| NEW | 스레드가 생성되고 아직 start()가 호출되지 않은 상태 |
| RUNNABLE | 실행 중 또는 실행 가능한 상태 |
| BLOCKED | 동기화 블럭에 의해 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태) |
| WAITING | 다른 스레드가 통지(notify)하거나 작업이 완료될 때까지 기다리는 상태 |
| TIMED_WAITING | 주어진 시간 동안 기다리는 상태 (sleep, timeout이 있는 join 등) |
| TERMINATED | 스레드의 작업이 종료된 상태 |
스레드 라이프사이클
Section titled “스레드 라이프사이클”stateDiagram-v2 direction LR state "생성 (NEW)" as NEW state "실행 대기 (RUNNABLE)" as RUNNABLE state "실행 (RUNNING)" as RUNNING state "일시 정지 (WAITING, BLOCKED)" as WAITING state "소멸 (TERMINATED)" as TERMINATED [*] --> NEW NEW --> RUNNABLE: ① start() RUNNABLE --> RUNNING: ② 스케줄링(실행) RUNNING --> RUNNABLE: ③ yield() RUNNING --> WAITING: ④ suspend(), sleep(),\nwait(), join(), I/O block WAITING --> RUNNABLE: ⑤ time-out, resume(),\nnotify(), interrupt() RUNNING --> TERMINATED: ⑥ stop() TERMINATED --> [*]- 스레드 생성하고
start()호출하여 실행 대기열에 저장되어 실행 대기 상태로 만듬- 실행대기열은 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 스레드가 먼저 실행됨
- 실행 대기열에 있다가 차례가 되면 실행상태로 변경
- 주어진 실행시간이 다되거나
yield()를 만나면 실행대기상태가 되고 다시 실행대기열에 들어감 - 실행 중
suspend(),sleep(),join(),wait(),I/O block의해 일시정지 상태가 될 수 있음 - 지정된 일시정지시간이 다 되거나
time-out(),notify(),resume(),interrupt()등의 메서드를 호출하면 다시 실행대기상태가 됨 - 실행을 모두 마치거나
stop()을 호출하면 종료상태가 됨
동시성 관련 유틸리티
Section titled “동시성 관련 유틸리티”ExecutorService와 스레드 풀
Section titled “ExecutorService와 스레드 풀”ExecutorService는 스레드 풀(Thread Pool)을 통해 스레드를 재사용하고 개수를 제한하는 인터페이스다.
- 스레드를 미리 생성해두고 재사용하여 생성·소멸 비용 절감
- 최대 스레드 수를 제한하여 시스템 자원 고갈 방지
- 작업 제출(
submit)과 실행 방식을 분리하여 코드 구조 단순화
Executors 팩토리 클래스로 목적에 맞는 풀을 생성한다.
| 메서드 | 스레드 수 | 특징 | 주의점 |
|---|---|---|---|
newFixedThreadPool(n) | 고정 (n개) | 처리량 예측 가능, 초과 작업은 큐에 대기 | 과부하 시 내부 큐 무한 증가 가능 |
newCachedThreadPool() | 무제한 (유동) | 유휴 스레드 재사용, 짧은 작업에 유리 | 순간 부하 시 스레드 수 폭발 위험 |
newSingleThreadExecutor() | 1개 고정 | 작업 순서 보장 | 처리 속도가 제출 속도를 못 따라가면 큐 누적 |
shutdown()을 호출하지 않으면 풀 스레드가 종료되지 않아 애플리케이션이 정상 종료되지 않으니 주의해야한다.
void example() { try (ExecutorService executor = Executors.newFixedThreadPool(4)) { for (int i = 0; i < 10; i++) { int taskId = i; executor.submit(() -> System.out.println("Task " + taskId)); } } // try-with-resources로 shutdown() 자동 호출}Callable과 Future
Section titled “Callable과 Future”ExecutorService.submit(Callable)은 Future<V>를 반환하며, Future를 통해 비동기 작업의 결과를 나중에 수집할 수 있다.
void example() { ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(() -> { // 시간이 걸리는 연산 return 42; });
// 다른 작업 수행 가능 ...
Integer result = future.get(); // 작업 완료 시까지 블로킹 executor.shutdown();}Future의 주요 메서드는 다음과 같다.
get(): 작업 완료 시까지 현재 스레드를 블로킹하고 결과 반환get(timeout, unit): 지정 시간까지만 대기, 초과 시TimeoutException발생isDone(): 작업 완료 여부 확인 (블로킹 없음)cancel(mayInterruptIfRunning): 작업 취소 요청
get() 호출 시점까지 결과가 준비되지 않으면 블로킹되므로, 여러 Future를 순서대로 get()하면 직렬 대기와 다르지 않다는 점에 주의해야 한다.