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

들어가며

java.util.concurrent 패키지에서 간단히 작업 큐를 생성할 수 있다.

1
2
3
4
5
ExecutorService exec = Executors.newSingleThreadExecutor(); //생성

exec.execute(runnable); //작업 할당

exec.shutdown(); //종료

그 외에도 다양한 기능을 지원한다.

  1. 특정 태스크가 완료되기를 기다린다.
  2. 태스크 모음중 아무것 하나(invokeAny) 혹은 모든 태스크(invokeAll)가 완료되기를 기다린다.
  3. ExecutorService가 종료하기를 기다린다.(awaitTermination)
  4. 완료된 태스크들의 결과를 차례로 받는다.(ExecutorCompletionService)
  5. 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다.(ScheduledThreadPoolExecutor)

큐를 둘 이상의 스레드가 처리하게 하고 싶다면 스레드 풀을 이용하면 된다. 더 세세한 설정을 원하면 ThreadPoolExecutor 클래스를 이용할 수 있다. 작은 프로그램이나 서버라면 Executors.newCachedThreadPool이 좋은 선택이다. 무거운 서버라면 이는 좋지 못하다. 새로운 태스크가 들어온다면 즉시 스레드에 위임되는데 가용할 스레드가 없다면 스레드를 새로 생성한다. 이는 곧 CPU 점유율을 100%로 올리다가 떨어뜨리는 결과를 낳게 되는데 떨어지는 이유는 context switching에 자원을 소모하기 때문이다. 따라서 무거운 서버라면 Executors.newFixedThreadPool이나 ThreadPoolExecutor를 직접 사용하는게 좋다.

태스크는 Runnable과 Callable이 있다. Callable은 Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다. 그리고 이 태스크를 수행하는 일반적인 매커니즘이 ExecutorService다. 태스크를 ExecutorService에 맡기면 원하는 태스크 수행 정책을 선택할 수 있고 언제든지 변경이 가능하다.

자바 7부터는 ExecutorService가 fork-join 태스크를 지원한다. fork-join 태스크는 fork-join pool이라는 특별한 ExecutorService가 실행해준다. ForkJoinTask의 인스턴스는 작은 하위 태스크로 나뉠 수 있고 FockJoinPool을 구성하는 스레드들이 이 태스크를 처리하며 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다. 이를 통해 CPU 처리량을 높히며 낮은 지연율을 달성할 수 있다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/23/Languages/Effective%20JAVA/item80/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.