Callable
자바에서 Callable 인터페이스는 멀티스레드 프로그래밍에서 비동기적으로 실행할 작업을 나타내는데 사용됩니다. Runnable 인터페이스와 유사하게 작동하지만, Callable은 작업 실행 후 결과를 반환할 수 있으며, 실행 중에 예외를 던질 수 있다는 점에서 차이가 있습니다.
기본 구조
Callable 인터페이스는 java.util.concurrent 패키지에 정의되어 있으며, 다음과 같은 단일 메서드를 가지고 있습니다:
public interface Callable<V> {
V call() throws Exception;
}
여기서 V는 메서드가 반환할 결과의 타입입니다.
사용 방법
Callable 인터페이스를 구현하는 클래스는 call() 메서드를 구현해야 하며, 이 메서드는 비동기적으로 실행될 작업의 내용을 정의합니다. call() 메서드는 작업의 실행이 완료되면 결과를 반환합니다.
Callable 객체는 보통 ExecutorService를 통해 실행됩니다. ExecutorService의 submit(Callable<V> task) 메서드를 사용하여 Callable 객체를 실행할 수 있으며, 이 메서드는 Future<V> 객체를 반환합니다. Future<V> 객체를 사용하여 Callable 작업의 결과를 나중에 검색할 수 있습니다.
예제
import java.util.concurrent.*;
public class Main {
static ExecutorService executor = Executors.newCachedThreadPool();
static Callable<Integer> callableTask = () -> {
System.out.println("Run callableTask");
return 123;
};
static Runnable runnableTask = () -> {
System.out.println("Run runnableTask");
};
public static void runCallableTask() {
Future<Integer> future = executor.submit(callableTask);
try {
Integer result = future.get();
System.out.println("callable task 결과: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public static void runRunnableTask() {
executor.execute(runnableTask);
}
public static void main(String[] args) {
runCallableTask();
runRunnableTask();
executor.shutdown();
System.out.println("main thread goodbye!");
}
}
특징
- Callable을 사용하면 실행 중인 작업으로부터 결과를 반환받을 수 있습니다.
- Callable은 예외를 처리하기 용이하게 해줍니다. call() 메서드는 예외를 던질 수 있으며, 이 예외는 Future.get() 메서드를 호출할 때 ExecutionException으로 포장되어 던져집니다.
- Future.get() 메서드는 작업이 완료될 때까지 현재 스레드를 블록합니다. 필요한 경우 Future.get(long timeout, TimeUnit unit) 메서드를 사용하여 최대 대기 시간을 지정할 수 있습니다.
Callable은 결과를 반환하거나 예외 처리가 필요한 복잡한 비동기 작업을 수행할 때 유용합니다.
ExecutorService
ExecutorService는 자바의 java.util.concurrent 패키지에 있는 인터페이스로, 스레드 풀을 관리하고 비동기적으로 태스크를 실행하는 메커니즘을 제공합니다. 이 인터페이스를 사용하면 멀티스레드 애플리케이션을 더 쉽게 구현할 수 있으며, 스레드의 생성, 실행, 종료 등을 직접 관리하는 복잡성을 추상화합니다.
주요 기능
- 태스크 실행: `ExecutorService`는 `Runnable`과 `Callable` 인터페이스를 구현하는 태스크를 비동기적으로 실행할 수 있습니다. `Callable` 태스크는 실행 결과를 반환할 수 있으며, 예외 처리도 가능합니다.
- 스레드 풀 관리: 스레드 풀을 사용하여 태스크를 실행함으로써, 스레드의 생성과 소멸에 드는 비용을 줄이고, 시스템 리소스를 효율적으로 사용할 수 있습니다.
- 태스크 결과 관리: `Future` 인터페이스를 통해 비동기 실행의 결과를 관리하고, 필요한 경우 결과가 준비될 때까지 대기하거나 태스크 실행을 취소할 수 있습니다.
- 생명주기 관리: `ExecutorService`는 실행 중인 모든 태스크를 셧다운하고 스레드 풀을 종료하는 메커니즘을 제공합니다. 이를 통해 애플리케이션이 종료될 때 자원을 적절히 정리할 수 있습니다.
사용 방법
ExecutorService 인스턴스는 보통 Executors 유틸리티 클래스의 팩토리 메소드를 통해 생성합니다. 다음은 몇 가지 예시입니다:
ExecutorService executor = Executors.newSingleThreadExecutor(); // 단일 스레드 풀
ExecutorService executor = Executors.newFixedThreadPool(10); // 고정 크기 스레드 풀
ExecutorService executor = Executors.newCachedThreadPool(); // 요청에 따라 스레드 수가 변하는 풀
태스크를 실행하기 위해 submit 메소드를 사용할 수 있으며, Future 객체를 반환받아 태스크의 결과나 상태를 확인할 수 있습니다.
예제
package org.example.executorservice;
import java.util.concurrent.*;
public class Main {
static ExecutorService executor = Executors.newCachedThreadPool();
static Callable<Integer> callableTask = () -> {
System.out.println("Run callableTask");
return 123;
};
static Runnable runnableTask = () -> {
System.out.println("Run runnableTask");
};
public static void runCallableTask() {
Future<Integer> future = executor.submit(callableTask);
try {
Integer result = future.get();
System.out.println("callable task 결과: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public static void runRunnableTask() {
executor.execute(runnableTask);
}
public static void main(String[] args) {
runCallableTask();
runRunnableTask();
executor.shutdown();
System.out.println("main thread goodbye!");
}
}
ExecutorService를 사용하면 스레드 관리의 복잡성을 크게 줄이면서, 비동기 태스크를 효율적으로 실행하고 관리할 수 있습니다.
ScheduledExecutorService
ScheduledExecutorService는 자바의 java.util.concurrent 패키지에 있는 ExecutorService를 확장한 인터페이스로, 스케줄링된 방식으로 태스크를 실행할 수 있는 기능을 제공합니다. 이 인터페이스를 사용하여 단일 실행 또는 반복 실행 태스크를 지정된 지연 시간 또는 주기적으로 실행할 수 있습니다. ScheduledExecutorService는 타이머나 스케줄러의 기능을 멀티스레드 환경에서 사용하기 위한 고수준의 추상화를 제공합니다.
주요 메서드
ScheduledExecutorService는 다음과 같은 주요 메서드를 제공합니다:
- schedule(Callable<V> callable, long delay, TimeUnit unit): 지정된 지연 시간 후에 Callable 태스크를 한 번 실행합니다.
- schedule(Runnable command, long delay, TimeUnit unit): 지정된 지연 시간 후에 Runnable 태스크를 한 번 실행합니다.
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 초기 지연 시간이 지난 후 주어진 주기마다 Runnable 태스크를 반복 실행합니다. 태스크의 실행 시간이 주기보다 길 경우, 다음 태스크는 이전 태스크가 끝난 직후 즉시 실행됩니다.
- scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit): 초기 지연 시간이 지난 후 태스크가 실행을 마친 후 주어진 지연 시간을 기다린 후 Runnable 태스크를 반복 실행합니다.
사용 예제
package org.example.schexecutorservice;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Runnable 태스크 실행: " + new Date());
Callable<String> callableTask = () -> {
System.out.println("Callable 태스크 실행: " + new Date());
return "결과";
};
// Runnable 태스크를 5초 후에 실행
scheduler.schedule(task, 5, TimeUnit.SECONDS);
// Callable 태스크를 10초 후에 실행
Future<String> result = scheduler.schedule(callableTask, 10, TimeUnit.SECONDS);
// 초기 지연 시간 없이, 2초마다 반복 실행
scheduler.scheduleAtFixedRate(task, 0, 2, TimeUnit.SECONDS);
// 태스크 실행 완료 후 3초 지연을 두고 반복 실행
scheduler.scheduleWithFixedDelay(task, 0, 3, TimeUnit.SECONDS);
// 예정된 태스크들이 실행되는 동안 대기
Thread.sleep(15000); // 15초 동안 실행
// 스케줄러 종료
scheduler.shutdownNow();
System.out.println("스케줄러 종료.");
// 스케줄러가 완전히 종료될 때까지 대기
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("스케줄러가 지정된 시간 내에 종료되지 않았습니다.");
}
// Callable 태스크의 결과가 준비되었는지 확인하고 출력
if (result.isDone()) {
System.out.println("Callable 태스크의 결과: " + result.get());
} else {
System.out.println("Callable 태스크의 결과가 아직 준비되지 않았습니다.");
}
}
}
주의 사항
- scheduleAtFixedRate와 scheduleWithFixedDelay 메서드를 사용할 때, 태스크의 실행 시간이 예상보다 길어지는 경우 예기치 않은 동작이 발생할 수 있으므로 주의가 필요합니다.
- 스케줄러를 더 이상 사용하지 않을 때는 반드시 shutdown() 메서드를 호출하여 스레드 풀을 종료해야 합니다. 그렇지 않으면 프로그램이 종료되지 않는 문제가 발생할 수 있습니다.
ScheduledExecutorService는 정확한 타이밍이나 주기적인 작업 실행이 필요한 애플리케이션에 유용하게 사용될 수 있습니다.
Thread Join
public class JoinExample {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable(), "Thread 1");
Thread thread2 = new Thread(new MyRunnable(), "Thread 2");
// Start thread1
thread1.start();
try {
// Wait for thread1 to complete
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Start thread2 after thread1 completes
thread2.start();
}
}
class MyRunnable implements Runnable {
public void run() {
try {
// Perform some task
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
'Java' 카테고리의 다른 글
Functional Interface (0) | 2024.04.09 |
---|---|
자바 예외 처리 (0) | 2024.04.09 |
Generics 4 (0) | 2024.04.09 |
Jenerics 3 (0) | 2024.04.09 |
Jenerics 2 (0) | 2024.04.09 |