[Java] Item 80 : 스레드보다는 실행자, 태스크, 스트림을 애용하라
Effective Java를 읽고 정리한 정리본입니다.
📌 Item 80 : 스레드보다는 실행자, 태스크, 스트림을 애용하라
java.util.concurrent 패키지는 동시성을 보장해주는 다양한 클래스를 담고 있는 패키지로, 실행자 프레임워크 (Executor Framework)라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
ExecutorService exec = Executors.newSingleThreadExecutor();
이는 작업 큐를 생성하는 방식 중 일부이다.
자바의 코드를 까 보면, newSingleThreadExecutor() 함수는 다음과 같은 함수를 호출하게 된다.
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new AutoShutdownDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
다음 코드는 이 실행자에 실행할 태스크를 넘기는 방법이다.
exec.execute(runnable);
또한, 종료시키는 법은 다음과 같다.
exec.shutdown();
이 작업이 실패해도 VM 자체가 종료되지는 않는다.
🫧 멀티 스레드 환경에서의 작업 큐
만일 둘 이상의 스레드가 큐를 처리하게 하고 싶다면 위에 봐았듯이, java.util.concurrent.Executors의 정적 팩터리들을 이용하면 손쉽게 생성이 가능하다.
또는, ThreadPoolExecutor 클래스를 이용해 더 세밀한 스레드 풀에 대한 속성을 결정할 수 있을 것이다.
그러나 CachedThreadPool은 무거운 프로덕션 서버에서는 사용하지 말자.
이는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행되며 가용한 스레드가 없다면 새로 하나를 생성하는 특성을 가졌는데, 이 때문에 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 CPU 이용률이 100%를 치솟을 것이다.
따라서 무거운 프로덕션 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나, 완전히 통제할 수 있는 ThreadPoolExecutor을 직접 사용하는 편이 낫다.
🫧 태스크
스레드를 직접 다루면 Thread가 작업 단위와 수행 메커니즘 역할을 모두 수행하게 되는 반면, 실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다는 장점이 있다.
이때 작업 단위를 나타내는 핵심 추상 개념이 태스크이다.
태스크에는 두 가지가 있는데, 한 번쯤은 들어보았을 Runnable과 Callable이다.
이를 수행하는 일반적인 메커니즘이 바로 실행자 서비스가 되는 것이다.