티스토리 뷰

스레드를 직접 다루는 것은 일반적으로 삼가야 한다.

✨ 실행자 프레임워크

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

과거에는 단순한 작업 큐를 만들기 위해서 많은 코드를 작성해야 했지만, 이젠 아래와 같이 간단하게 작업 큐를 생성할 수 있다.

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

//이 실행자에 실행할 태스크를 넘기는 방법
exec.execute(runnable);

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

실행자 서비스는 이 외에도 여러 주요 기능들을 가지고 있다.

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

 

👻 Thread Pool

큐를 둘 이상의 스레드가 처리하게 하고 싶다면,

다른 정적 팩토리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다.

 

스레드 풀의 스레드 개수는 고정할 수도 있고 필요에 따라 늘어나거나 줄어들게 설정할 수 있다.

필요한 실행자 대부분은 java.util.concurrent.Executors의 정적 팩토리들을 이용해 생성할 수 있다.

 

평범하지 않은 실행자를 원한다면 ThreadPoolExecutor 클래스를 직접 사용해도 된다.

 

🔗 Executors.newCachedThreadPool

실행자 서비스를 사용하기에 까다로운 애플리케이션도 있다. 

작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool이 일반적으로 좋은 선택이다.

특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다.

 

하지만 무거운 프로덕션 서버에는 CPU 이용률이 100%로 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하기 때문에 상황을 더욱 악화시킨다.

 

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

 

📚 Runnable & Callable

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

스레드를 직접 다루면 Thread가 작업 단위와 수행 매커니즘 역할을 모두 수행하게 된다.

 

반면 실행자 프레임워크에서는 작업 단위와 실행 매커니즘이 분리된다.

작업 단위를 나타내는 핵심 추상 개념이 태스크인데, 태스크에는 Runnable과 Callable이 있다.

(Callable은 Runnable과 비슷하지만, 값을 반환하고 임의의 예외를 던질 수 있다)

 

그리고 이러한 태스크를 수행하는 일반적인 메커니즘이 바로 실행자 서비스이다.

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

또한 실행자 프레임워크가 작업 수행을 담당해준다.

 

📢 포크-조인(fork-join)

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

포크 조인 태스크는 포크 조인 풀이라는 특별한 실행자 서비스가 실행해준다.

ForkJoinTask의 인스턴스는 작은 하위 태스크로 나뉠 수 있고,

ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리하며,

일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다.

 

이렇게 하여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다.

댓글
공지사항
최근에 올라온 글