아이템 80. 스레드보다는 실행자, 태스트, 스트림을 애용하라. - ksw6169/effective-java GitHub Wiki
실행자 프레임워크(Executor framework)
- java.util.concurrent 패키지에 포함된 기능으로 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
- Executor(ex. ThreadPoolExecutor)는 제공된 작업을 실행하는 객체가 구현해야할 인터페이스로 작업을 제공하는 코드와 작업을 실행하는 매커니즘 사이의 커플링을 제거해준다.
- ExecutorService는 Executor의 라이프 사이클을 관리할 수 있는 기능을 정의하는 인터페이스이다.
// 작업 큐(work queue) 생성
ExecutorService exec = Executors.newSingleThreadExecutor();
// 실행자에서 실행할 태스크(작업)를 넘기는 방법
exec.execute(runnable);
// 실행자 종료(이 작업이 실패하면 VM 자체가 종료되지 않는다.)
exec.shutdown();
ExecutorService의 주요 기능
- 특정 태스크가 완료되기를 기다린다.(get())
- 태스크 모음 중 아무것 하나(invokeAny()) 혹은 모든 태스크(invokeAll())가 완료되기를 기다린다.
- 실행자 서비스가 종료하기를 기다린다. (awaitTermination())
- 완료된 태스크들의 결과를 차례로 받는다. (ExecutorCompletionService 이용)
- 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다. (ScheduledThreadPoolExecutor 이용)
ExecutorService 생성
- 큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩토리(Executors의 정적 팩토리)를 이용하여 다른 종류의 실행자 서비스를 생성하면 된다.
- 스레드 풀의 스레드 개수는 고정할 수도 있고 필요에 따라 늘어나거나 줄어들게 설정할 수도 있다.
- 대부분의 ExecutorService는 Executors의 정적 팩토리를 이용해 생성할 수 있을 것이다.
- 만약 원한다면 ThreadPoolExecutor 클래스를 직접 사용해도 된다. 이 클래스로는 스레드 풀 동작을 결정하는 거의 모든 속성을 설정할 수 있다.
어떤 ExecutorService를 써야 할까?
- 작은 프로그램이나 가벼운 서버라면 CachedThreadPool이 일반적으로 좋은 선택일 것이다. 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다.
- 하지만 무거운 프로덕션 서버에는 좋지 못하다. CachedThreadPool 에서는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행된다. 가용한 스레드가 없으면 새로 하나를 생성한다. 서버가 아주 무겁다면 CPU 이용률이 100%로 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 상황을 더욱 악화시킨다.
- 따라서 프로덕션 서버에서는 스레드 개수를 고정한 FixedThreadPool을 선택하거나 완전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 훨씬 낫다.
ExecutorService 사용 시 주의사항
- 작업 큐를 손수 만드는 일은 삼가야 하고, 스레드를 직접 다루는 것도 일반적으로 삼가야 한다.
- 스레드를 직접 다루면 Thread가 작업 단위와 수행 메커니즘 역할을 모두 수행하게 된다.
- 반면 실행자 프레임워크 에서는 작업 단위와 실행 메커니즘이 분리된다.
Task
- Task란 작업 단위를 나타내는 핵심 추상 개념이다.
- Task에는 Runnable과 Callable이 있다. (Callable은 Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다.)
- 그리고 Task를 실행하는 일반적인 메커니즘이 바로 ExecutorService다.
- Task 수행을 ExecutorService에 맡기면 원하는 Task 수행 정책을 선택할 수 있고, 생각이 바뀌면 언제든지 변경할 수 있다. 핵심은 컬렉션 프레임워크가 데이터 모음을 담당하듯 ExecutorService가 작업 수행을 담당해준다는 것이다.
ForkJoinTask
- 자바 7이 되면서 실행자 프레임워크는 ForkJoinTask를 지원하도록 확장되었다.
- ForkJoinTask는 ForkJoinPool이라는 특별한 실행자 서비스가 실행해준다.
- ForkJoinTask의 인스턴스는 작은 하위 Task로 나뉠 수 있고, ForkJoinPool을 구성하는 스레드들이 이 Task들을 처리하며, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 Task를 가져와 대신 처리할 수도 있다.
- 이렇게 하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다.
- 이러한 ForkJoinTask를 직접 작성하고 튜닝하기란 어려운 일지만, ForkJoinPool을 이용해 만든 병렬 스트림을 이용하면 적은 노력으로 그 이점을 얻을 수 있다.
- 물론 Fork-Join에 적합한 형태의 작업이어야 한다.
참고 자료