본문은 Effective Java를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
선결론
스레드를 직접 관리하면서 작업을 수행하기보다는 이미 제공된 기술들을 적극적으로 활용하자
작업 큐를 손수 만들거나 스레드를 직접 다루는 일은 지양하자
실행자 프레임워크의 구성요소
자바는 비동기/병렬 처리를 위해 java.util.concurrent에서 실행자 프레임워크(Executor Framework)를 제공한다
실행자 프레임워크를 사용하면 스레드의 생성과 재사용을 쉽게 할 수 있기 때문에 Runnable 객체를 처리하는 데 유용하다
Executor 인터페이스
넘겨받은 Runnable 태스크를 실행하는 역할을 한다
public interface Executor {
void execute(Runnable command);
}
Excecutor 인터페이스를 사용하면 작업 단위와 실행 메커니즘이 분리된다
// 스레드를 직접 다루는 방식: 스레드가 작업 단위와 실행 메커니즘 역할을 모두 수행
// 쓰레드와 작업이 1:1
new Thread(new(RunnableTask1())).start()
new Thread(new(RunnableTask2())).start()
// Executor 활용: 작업 단위와 실행 메커니즘이 분리됨
// 쓰레드와 작업이 M:N, 프로그래머는 작업을 실행시키고 내부에서 적절하게 처리됨
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...
ExecutorService 인터페이스
Executor를 확장한 인터페이스로서 종료를 수행하는 메서드들을 제공하고 비동기 작업의 경과를 추적하는 Future 객체를 생성할 수 있다
다음은 ExecutorService의 주요 기능들이다
특정 태스크가 완료되기를 기다린다(get)
태스크 모음 중 아무 것 하나가 완료되기를 기다린다(invokeAny)
모든 태스크가 완료되기를 기다린다(awaitTermination)
실행자 서비스가 종료하기를 기다린다(awaitTermination 메서드)
완료된 태스크들의 결과를 차례로 받는다(ExecutorCompletionService 이용)
태스크를 특정 시간에 혹은 주기적으로 실행하게 한다(ScheduledThreadPoolExecutor 이용)
Executors 클래스
Excutors는 쓰레드풀 생성을 할 수 있는 다양한 정적 팩터리 메서드를 제공한다
newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool 등
newCachedThradPool은 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임하게 되는데, 이 과정에서 스레드가 계속해서 생성되어 서버에 부하를 줄 수 있다
Executors 대신 ThreadPoolExecutor를 직접 사용하여 스레드 풀을 만들 수 있지만, 이는 스레드 풀 동작을 결정하기 위한 속성들을 일일히 세팅해줘야 하므로 번거롭다
실제로 Executors 내부의 정적 팩터리 메서드들 중 몇몇은 ThreadPoolExecutor에 속성을 세팅한 후 반환한다
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
fork-join 태스크
자바 7부터 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원한다
fork-join 태스크는 ForkJoinPool이라는 ExecutorService가 실행해준다(이 실행자 서비스의 생성 또한 Executors 클래스에서 지원해준다)
fork-join 태스크(ForkJoinTask의 인스턴스)는 작은 하위 태스크로 나뉠 수 있고, ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리하며, 일을 먼저 끝내는 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다
즉, CPU를 최대한 활용한다
ForkJoinPool을 이용해 만든 병렬 스트림(아이템 48)을 이용하면 이러한 작업을 쉽게 수행할 수 있다