Thread를 구현하는 2가지 방법
멀티스레딩 프로그래밍에서 사용되는 인터페이스
1. Runnable 인터페이스 구현
2. Callable 인터페이스 구현
- Runnable은 return value가 없기 때문에 ExecutorService에서 비동기로 처리해도 문제가 없었다.
- 하지만 Callable은 return value가 있고, Exception도 발생할 수 있으므로 ExecutorService에서 submit()을 할 때 해당 task가 완료될 때까지 기다리지 않으므로 ExecutorService는 결과를 직접 반환할 수 없다. 그래서 Future라는 특수한 result type을 반환하고, Future를 통해서 나중에 실제 실행 결과를 검색할 수 있다.
Runnable & Callable Interface Code
1. Runnable
public interface Runnable {
public abstract void run();
}
2. Callable
- 인자를 받지 않고, 특정 타입의 객체를 리턴한다.
- call() 수행 중 Exception을 발생시킬 수 있다.
public interface Callable {
V call() throws Exception;
}
- 수행 코드
import java.util.concurrent.*;
public class CallableAsyncExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
System.out.println("메인 스레드 시작");
Callable<String> task = () -> {
Thread.sleep(2000);
return "비동기 작업 완료!";
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(task); // 비동기 실행 및 결과 대기 가능
System.out.println("메인 스레드에서 다른 작업 수행 중...");
// 결과를 기다릴 필요가 없으나, 필요할 때 get() 호출
String result = future.get(); // 결과를 기다림 (블로킹)
System.out.println("결과: " + result);
executor.shutdown();
}
}
ExecutorService에서 Callable 사용 방법
- ExecutorService에서 Callable을 Job으로 등록하고 수행시킬 수 있다.
CompletableFuture
- 비동기 작업 수행: 메인 스레드와 별도로 작업을 수행할 수 있다.
- 결과 반환 가능: 작업 완료 후 값을 반환할 수 있다.
- 체이닝 지원: thenApply, thenAccept 등을 사용하여 연속적인 작업 수행 가능.
- 예외 처리 가능: exceptionally, handle 등을 통해 예외를 쉽게 처리할 수 있다.
- 논블로킹(non-blocking): Future와 달리 get() 호출 없이도 작업을 연결할 수 있다.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
System.out.println("메인 스레드 시작");
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000); // 비동기 작업
System.out.println("비동기 작업 완료");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("메인 스레드에서 다른 작업 수행 중...");
// 결과를 기다리지 않고 비동기 작업 진행
future.join(); // (옵션) 메인 스레드 종료 전 비동기 작업 완료 대기
System.out.println("메인 스레드 종료");
}
}
//결과
메인 스레드 시작
메인 스레드에서 다른 작업 수행 중...
비동기 작업 완료
메인 스레드 종료
🎯 정리: Runnable vs Callable vs CompletableFuture
특징 | Runnable | Callable | CompletableFuture |
비동기 지원 여부 | 지원 가능 (결과 없음) | 지원 가능 (결과 있음) | 완전 비동기 지원 (결과 있음) |
결과 반환 여부 | 없음 | 있음 | 있음 |
예외 처리 | Checked Exception 미지원 | Checked Exception 지원 | Checked Exception 지원 |
스레드 풀 관리 | 수동 (Thread) | ExecutorService 사용 | ExecutorService 사용 |
Java 버전 | 1.0 이상 | 1.5 이상 | 8 이상 |
ExecutorService란?
- ExecutorService는 멀티스레딩을 간편하게 다룰 수 있게 해주는 java.util.concurrent 패키지에서 제공하는 스레드 풀 관리를 위한 API.
- 스레드 풀 관리로 성능 최적화 가능.
- 비동기 작업 수행 및 결과 반환을 Runnable, Callable과 함께 지원.
- 적절한 스레드 풀 선택이 중요 (Fixed, Cached, SingleThread, Scheduled).
ExecutorService는 멀티스레딩의 복잡성을 줄이고, 효율적인 스레드 관리를 제공한다.
✅ 기본 개념
- 스레드 풀 관리: 여러 스레드를 미리 생성해두고 필요할 때 재사용하는 방식.
- 비동기 작업 지원: 여러 작업을 동시에 수행 가능.
- 스레드 수 제한: 스레드 수를 제한하여 과도한 스레드 생성 방지.
- 작업 제출 및 실행: Runnable, Callable을 실행하고, Future를 반환할 수 있음.
- 자동 종료 지원 안함: shutdown()을 호출해야 스레드 종료.
📌 기본 사용법
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
// 고정 스레드 풀 생성 (스레드 3개)
ExecutorService executor = Executors.newFixedThreadPool(3);
// 작업 제출
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("작업 " + taskId + " 실행, 스레드: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 스레드 풀 종료 (더 이상 작업을 받지 않음)
executor.shutdown();
}
}
//출력결과 (실행 순서는 비동기이므로 다를 수 있음)
작업 1 실행, 스레드: pool-1-thread-1
작업 2 실행, 스레드: pool-1-thread-2
작업 3 실행, 스레드: pool-1-thread-3
작업 4 실행, 스레드: pool-1-thread-1
작업 5 실행, 스레드: pool-1-thread-2
📌 스레드 풀 종류
자바의 Executors 클래스는 다양한 ExecutorService를 제공합니다.
1. 고정 크기 스레드 풀 (newFixedThreadPool)
ExecutorService executor = Executors.newFixedThreadPool(3);
- 지정한 수의 스레드만 생성하고, 초과 작업은 대기 상태로 유지.
- CPU 코어 수에 맞춰 사용하기 적합.
2. 캐시된 스레드 풀 (newCachedThreadPool)
ExecutorService executor = Executors.newCachedThreadPool();
- 필요할 때 새로운 스레드를 생성하고, 유휴 상태의 스레드는 재사용.
- 많은 단기 작업에 적합.
3. 단일 스레드 풀 (newSingleThreadExecutor)
ExecutorService executor = Executors.newSingleThreadExecutor();
- 단일 스레드만 사용.
- 작업을 순차적으로 실행.
4. 스케줄링된 스레드 풀 (newScheduledThreadPool)
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorExample {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// 3초 후에 실행
scheduler.schedule(() -> System.out.println("3초 후 실행!"), 3, TimeUnit.SECONDS);
// 1초 간격으로 반복 실행 (5초 후 시작)
scheduler.scheduleAtFixedRate(() -> System.out.println("1초 간격 실행!"), 5, 1, TimeUnit.SECONDS);
}
}
📌 작업 제출 방식
ExecutorService는 Runnable, Callable을 사용해 작업을 제출할 수 있습니다.
1. Runnable (결과 없음)
2. Callable (결과 반환)
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
Callable<String> task = () -> {
Thread.sleep(1000);
return "Callable 작업 완료";
};
Future<String> result = executor.submit(task);
System.out.println("결과: " + result.get()); // 결과 대기
📌 스레드 풀 종료 (shutdown() vs shutdownNow() 차이점)
1. shutdown()
- 새로운 작업을 받지 않고, 진행 중인 작업은 완료.
2. shutdownNow()
- 현재 진행 중인 작업을 즉시 중단하고, 대기 중인 작업은 취소.
3. awaitTermination()
- 특정 시간 동안 기존 작업 완료 대기.
참고자료
https://winterbe.com/posts/2015/04/07/java8-concurrency-tutorial-thread-executor-examples/
https://codechacha.com/ko/java-callable-vs-runnable/
https://www.geeksforgeeks.org/what-is-java-executor-framework/
ExecutorService - chatGPT
댓글