요약

java.util.concurrent 패키지를 활용해 실행자 프레임워크를 사용하자

 

1. java.util.concurrent 패키지

이 패키지는 실행자 프레임워크라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다. 이 프레임워크를 사용하면 간단한 코드로 작업 큐 관련 작업을 수행할 수 있다.

실행자 코드

// 작업 큐 생성
ExecutorService exec = Executors.newSingleThreadExecutor();

// 실행자에게 태스크 넘기기
exec.execute(runnable);

// 실행자 종료
exec.shutdown();

실행자 서비스의 주요 기능들

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

java.util.concurrent.Executors

위 패키지의 정적 팩터리들을 이용하면 필요한 대부분의 실행자를 생성할 수 있다. 이 정적 팩터리로 실행자 서비스(스레드 풀)를 생성하면 스레드 개수도 유동적으로 설정할 수 있고, 큐를 둘 이상의 스레드 처리할 수 있게 설정할 수 있다.

서버 크기에 맞는 스레드 풀

CachedThreadPool(Executors.newCachedThreadPool)에서는 요청 받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행한다. 만약 가용한 스레드가 없다면 새로 하나 생성한다. 그렇기에 이 스레드 풀은 작은 프로그램이나 가벼운 서버에서만 사용하는 것이 좋다. 무거운 프로덕션 서버에서는 사용하면 안된다. 만약 무거운 프로덕션 서버에서 이 스레드 풀을 사용한다면 CPU 사용률이 100%으로 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성해 상황을 더욱 악화시킬 수 있기 때문이다.

따라서 무거운 프로덕션 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나 완전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 훨씬 낫다.

 

2. 태스크

실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다. 이때 작업 단위를 나타내는 핵심 추상 개념이 태스크이다.

Runnable, Callable

태스크에는 Runnable과 Callable이 있다.

실행자 서비스가 이런 태스크를 수행하는 일반적인 메커니즘이다. 실행자 프레임워크가 작업 수행을 담당해준다.

 

3. 포크-조인 태스크

자바 7부터 실행자 프레임워크는 포크-조인 태스크를 지원하도록 확장되었다.

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

→ 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용해 높은 처리량과 낮은 지연시간을 달성한다.

병렬 스트림

이런 포크-조인 태스크를 직접 작성하고 튜닝하는 것은 어렵기에 자바의 병렬 스트림을 사용하면 손쉽게 이점을 얻을 수 있다. (포크-조인에 적합한 형태의 작업일 때)

+ Recent posts