DEV ℧ Developer Diary

[EffectiveJava] item80 - 스레드보다는 실행자, 태스크, 스트림을 애용하라

실행자 프레임워크(ExecutorService)

이 책의 2판이 나오기 앞서 java.util.concurrent 패키지가 등장했다.

이 패키지는 실행자 프레임워크(Executor Framework)라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.

실행자 프레임워크는 클라이언트가 요청한 작업을 백그라운드 스레드에 위임해 비동기적으로 처리하는데, 앞서 구현한 초판의 코드보다 모든 면에서 뛰어난 작업 큐를 단 한줄로 생성할 수 있다.

작업 큐 생성

ExecutorService exec = Executors.newSingleThreadExecutor();

실행자에 실행할 태스크(task; 작업)을 넘기는 방법

exec.execute(runnable);

실행자를 우아하게 종료시키는 방법

exec.shutdown();

실행자 프레임워크의 주요 기능

  • 특정 태스크가 완료되기를 기다린다 (submit().get())
  • 태스크 모음 중 아무것 하나 (invokeAny 메서드) 혹은 모든 태스크 (invokeAll 메서드)가 완료되기를 기다린다.
  • 실행자 서비스가 종료하기를 기다린다 (awaitTermination 메서드)
  • 완료된 태스크들의 결과를 차례로 받는다 (ExecutorCompletionService)
  • 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다 (ScheduledThreadPoolExecutor)

ThreadPool

작업 큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩터리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다.

ThreadPoolExecutor

평범하지 않은 실행자를 원한다면 ThreadPoolExecutor 클래스를 직접 사용한다. 이 클래스로는 스레드 풀 동작을 결정하는 거의 모든 속성을 설정할 수 있다.

Executors.newCachedThreadPool

작은 프로그램이나 가벼운 서버일 경우 사용한다. 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다. 하지만 CachedThreadPool은 무거운 프로덕션 서버에는 좋지 못하다.

CachedThreadPool에서는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행된다. 가용한 스레드가 없다면 새로 하나를 생성한다.

서버가 아주 무겁다면 CPU 이용율이 100%로 치닫고, 새로운 태스크가 도착하는 족족 다른 스레드를 생성하며 상황을 더욱 악화시킨다.

Executors.newFixedThreadPool

무거운 프로덕션 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 사용하거나 ThreadPoolExecutor를 직접 사용하는 편이 훨씬 낫다.

작업 큐와 스레드의 주의할 점

작업 큐를 손수 만드는 일은 삼가야 하고, 스레드를 직접 다루는 것도 일반적으로 삼가야 한다.

스레드를 직접 다루면 Thread가 작업 단위와 수행 메커니즘을 모두 수행하게 된다. 반면 실행자 프레임 워크에서는 작업 단위와 실행 메커니즘이 분리된다.

태스크에는 두가지가 있는데, 바로 Runnable과 그 사촌인 Callable이다.

태스크 수행을 실행자 서비스에 맡기면 원하는 태스크 수행 정책을 선택할 수 있고, 언제든 변경할 수 있다.

핵심은 실행자 프레임워크가 작업 수행을 담당해준다는 것이다.

포크-조인(fork-join)

자바 7이 되면서 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원하도록 확장되었다.

포크-조인 태스크(ForkJoinTask)
포크-조인 풀이라는 특별한 실행자 서비스가 실행해준다. 인스턴스로는 작은 하위 태스크로 나뉠 수 있고, ForkJoinTask을 구성하는 스레드들이 이 태스크들을 처리하며, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수 있다.