java.util.concurrent
패키지는 실행자 프레임워크(Executor Framework) 라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
// 작업 큐를 생성하다.
ExcutorService exec = Executors.newSingleThreadExcutor();
// 다음은 이 Excutor에 실행할 태스크(task; 작업)를 넘기는 방법이다.
exec.execute(runnable);
// 다음은 Excutor를 우아하게 종료시키는 방법이다
// (이 작업이 실패하면 VM 자체가 종료되지 않을 것이다)
exec.shutdown();
- 특정 테스크가 완료되기를 기다린다.
- 태스크 모음 중 아무것 하나 (`invokeAny` 메서드) 혹은 모든 태스크(`invokeAll` 메서드)가 완료되기를 기다린다.
- 실행자 서비스가 종료하기를 기다린다(`awaitTermination` 메서드).
- 완료된 태스크들의 결과를 차례로 받는다(`ExecutorCompletionService` 이용).
- 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다(`ScheculedThreadPoolExecutor` 이용).
큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩토리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다.
import java.util.concurrent.*;
public class MultiThreadedQueueProcessing {
// 작업을 저장할 큐
private static BlockingQueue<String> queue = new LinkedBlockingQueue<>(); // 여러 스레드가 동시에 이 큐에 접근하여 작업을 추가하거나 꺼낼 수 있음
public static void main(String[] args) {
// 스레드 풀 생성
ExecutorService executorService = Executors.newFixedThreadPool(4); // 4개의 스레드로 구성된 스레드 풀
// 큐에 작업 추가
for (int i = 0; i < 10; i++) {
queue.add("Task " + i);
}
// 여러 스레드가 큐를 처리하도록 실행
for (int i = 0; i < 4; i++) {
executorService.execute(new QueueProcessor(queue));
// QueueProcessor: Runnable 인터페이스를 구현한 클래스
// run 메서드에서 큐의 작업을 처리
// 각 스레드는 큐에서 작업을 꺼내어 처리
}
// 스레드 풀 종료
executorService.shutdown();
}
// 큐를 처리하는 작업을 정의한 클래스
static class QueueProcessor implements Runnable {
private BlockingQueue<String> queue;
public QueueProcessor(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (!queue.isEmpty()) {
// 큐에서 작업을 꺼내 처리
String task = queue.take();
System.out.println(Thread.currentThread().getName() + " processing " + task);
// 작업 처리 시간을 시뮬레이션
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
평범하지 않은 실행자를 원한다면 ThreadPoolExecutor
클래스를 직접 사용해도 된다. 이 클래스로는 스레드 풀 동작을 결정하는 거의 모든 속성을 설정할 수 있다.
작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool
을 사용하는 것이 좋다.
CachedThreadPool
에서는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행된다.무거운 프로덕션 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool
을 선택하거나 완전히 통제할 수 있는 ThreadPoolExecutor
를 직접 사용하는 편이 훨씬 낫다.
작업 큐를 손수 만드는 일은 삼가야 하고, 스레드를 직접 다루는 것도 일반적으로 삼가야 한다.
스레드를 직접 다루면 Thread가 작업 단위와 수행 메커니즘 역할을 모두 수행하게 된다.
반면 실행자 프레임워크에서는 작업 단위와 실행 매커니즘이 분리되어 있어서 의미가 명확하다.