Runnable의 단점을 해결하기 위해 나온 인터페이스다.
주로 run() 메서드가 문제다. void라 명시되어 있기 때문에 값을 return은 쓸 수 있지만, 특정 타입 값을 반환할 수 없다.
게다가 checkedException을 던질 수가 없다.
public interface Callable<V> {
V call() throws Exception;
}
반면 Callable은 제네릭 타입과 예외를 던지기 때문에 단점을 해결해준다.
사용 방법도 간단한다.
import java.util.concurrent.*; // Executor 관련 클래스들을 한 번에 임포트
import java.util.List;
import java.util.ArrayList;
public class CallableExample {
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Callable 예제 시작 ---\n");
// 1. ExecutorService 생성 (스레드 풀)
// 여기서는 3개의 스레드를 가진 고정 스레드 풀을 생성합니다.
ExecutorService executor = Executors.newFixedThreadPool(3);
System.out.println("ExecutorService (고정 스레드 풀) 생성됨.");
// Future 객체를 저장할 리스트
List<Future<String>> futures = new ArrayList<>();
// 2. Callable 작업 정의 및 제출
for (int i = 1; i <= 5; i++) {
final int taskId = i; // 람다에서 사용하기 위해 final 또는 effectively final 변수
// 람다 표현식으로 Callable 인스턴스 생성
Callable<String> task = () -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": Task " + taskId + " 시작...");
long sleepTime = ThreadLocalRandom.current().nextInt(1, 4); // 1~3초 랜덤 대기
TimeUnit.SECONDS.sleep(sleepTime); // 비동기 작업 시뮬레이션
System.out.println(threadName + ": Task " + taskId + " 완료 (결과 반환 준비)");
return "Task " + taskId + " 완료! (" + threadName + ", " + sleepTime + "초 소요)"; // 결과 반환
};
// 3. 작업 제출 및 Future 객체 받기
Future<String> future = executor.submit(task);
futures.add(future);
System.out.println("Task " + taskId + " 제출됨. Future 객체 확보.");
}
System.out.println("\n--- 모든 작업 제출 완료. 결과 대기 중... ---\n");
// 4. Future 객체를 통해 작업 결과 조회
for (Future<String> future : futures) {
try {
// future.get()은 작업이 완료될 때까지 블로킹됩니다.
String result = future.get();
System.out.println(">>> 결과 수신: " + result);
} catch (InterruptedException e) {
// 현재 스레드가 다른 스레드에 의해 인터럽트되었을 때 발생
Thread.currentThread().interrupt();
System.err.println("작업 대기 중 인터럽트됨: " + e.getMessage());
} catch (ExecutionException e) {
// Callable의 call() 메서드에서 예외가 발생했을 때 발생
System.err.println("작업 실행 중 예외 발생: " + e.getCause().getMessage());
}
}
// 스레드 풀 종료 (새 작업을 더 이상 받지 않고 현재 작업 완료 대기)
executor.shutdown();
// 모든 작업이 종료될 때까지 최대 5초 대기
if (executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("\n--- 모든 Callable 작업이 성공적으로 종료되었습니다. ---");
} else {
System.out.println("\n--- 일부 Callable 작업이 시간 내에 종료되지 않았습니다. ---");
}
System.out.println("\n--- Callable 예제 종료 ---");
}
}
Runnable처럼 사용한 다음에 Callable<타입>만 명시해주면 된다.
주로 ExecutorService와 함께 사용되며, Future에 값을 반환하여 값을 사용한다.
Future에 대해 간단히 설명하지면 미래의 값을 반환받는 거다. 미리 반환받기 때문에 호출한 스레드는 다음 작업을 할 수 있다. 물론 결과값은 작업이 끝난 뒤에야 받을 수 있다. 사실상 join처럼 waiting 상태로 값이 나올 때까지 대기한다. Future 객체를 참조하여 Future<Integer(타입)> future = es(ExecutorService 이름).submit(new MyCallable()(Callable 이름)); submit이란 메서드를 이용하여 Future 안에 수행할 작업을 넣을 수 있다. 그러면 Future이 작업을 품고 Queue에 들어간다.