-
[Effective Java] 스레드보다는 실행자, 태스크, 스트림을 애용하라Java 2023. 2. 14. 02:43
스레드보다는 실행자, 태스크, 스트림을 애용하라
책 초판의 아이템 49에서는 단순한 작업 큐(work queue) 클래스는 클라이언트가 요청한 작업을 백그라운드 스레드에 위임해 비동기적으로 처리해줬다.
작업 큐가 필요 없어지면 클라이언트는 큐에 중단을 요청할 수 있고, 그러면 큐는 남아 있는 작업을 마저 완료한 후 스스로 종료한다.
안전 실패나 응답불가가 될 여자를 없애는 코드를 추가해야 했기 때문에 코드가 길고 복잡했다.
java.util.concurrent 패키지
java.tuil.concurrent 패키지가 등장하면서 더이상 위와 같은 코드를 작성하지 않아도 됐다.
이 패키지는 실행자 프레임워크라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
그래서 모든 면에서 뛰어난 작업 큐를 다음의 단 한줄로 생성할 수 있게 되었다.
// 작업 큐를 생성한다. ExecutorService exec = Executors.newSingletonThreadExecutor(); //다음은 이 실행자에 실행한 태스크(task: 작업)를 넘기는 방법이다. exec.execute(runnable) //다음은 실행자를 우아하게 종료시키는 방법이다 exec.shutdown();
실행자 서비스의 기능
- 특정 테스크가 완료되기를 기다린다.
- 태스크 모음 중 아무것 하나 혹은 모든 태스크가 완료되기를 기다린다.
- 실행자 서비스가 종료하기를 기다린다.
- 완료된 태스크들의 결과를 차례로 받는다.
- 태스크를 특정 시간에 혹은 주기적으로 실행하게 된다.
큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩터리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)을 생성하면 된다.
스레드 풀의 스레드 개수는 고정할 수도 있고, 필요에 따라 늘어나거나 줄어들게 설정할 수도 있다.필요한 실행자 대부분은 java.util.concurrent.Executors의 정적 팩터리들을 이용해 생성할 수 있을 것이다.
스레드 풀
스레드 풀의 스레드 개수는 고정할 수도 있고 필요에 따라 늘어나거나 줄어들게 설정할 수 있다.
우리가 필요한 실행자 대부분은 java.util.concurrent.Executors의 정적 팩토리들을 이용해 생성할 수 있다.실행자 프레임워크가 존재하니 작업 큐를 손수 만드는 일은 삼가고 스레드를 직접 다루는 것도 일반적으로 삼가자. 스레드를 직접 다루면 스레드가 작업 단위와 수행 메커니즘 역할을 모두 수행해야하는 반면, 실행자 프레임워크에선 작업 단위와 실행 메커니즘이 분리된다.
Executors.newCachedThreadPool
실행자 서비스를 사용하기에 까다로운 애플리케이션도 있다.
작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool이 일반적으로 좋은 선택일 것이다.
특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다.하지만 CachedThreadPool은 무거운 프로덕션 서버에는 좋지 못하다.
- CachedThreadPool에서는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행된다.
- 가용한 스레드가 없다면 새로 하나를 생성한다.
- 서버가 아주 무겁다면 CPU 이용률이 100%로 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 상황을 더욱 악화시킨다.
- 따라서 무거운 프로덕션 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나 완전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 훨씬 낫다.
- newFixedThreadPool 쓰레드를 고정적으로 몇개를 놀게 해줄지 정하는 쓰레드풀
- CachedThreadPool 유기적으로 쓰레드의 숫자가 증가하고 감소하는 쓰레드풀
실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다.
작업 단위를 나타내는 핵심 추상 개념이 태스크다.
- 태스크에는 두 가지가 있다 Runnable과 그 사촌인 Callable이다. (Calllable은 Runnable과 비슷하지만 값을 반환하고 임의의 예외를 던질 수 있다.)
- 그리고 태스크를 수행하는 일반적인 매커니즘이 바로 실행자 서비스다.
- 태스크 수행을 실행자 서비스에 맡기면 원하는 태스크 수행 정책을 선택할 수 있고, 생각이 바뀌면 언제든 변경할 수 있다.
- 핵심은 (컬렉션 프레임워크가 데이터 모음을 담당하듯) 실행자 프레임워크가 작업 수행을 담당해준다는 것이다.)
포크-조인 풀
자바 7이 되면서 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원하도록 확장되었다.
- 포크-조인 테스크는 포크-조인 풀이라는 특별한 실행자 서비스가 실행해준다.
- ForkJoinTask의 인스턴스는 작은 하위 태스크로 나뉠 수 있고. ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리할 수도 있다.
- 이렇게 하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다.
- 이러한 포크-조인 태스크를 직접 작성하고 튜닝하기란 어려운 일이지만, 포크-조인 풀을 이용해 만든 병렬 스트림(아이템48) 을 이용하면 적은 노력으로 그 이점을 얻을 수 있다.
참조
https://javabom.tistory.com/53?category=835783
https://hamait.tistory.com/612'Java' 카테고리의 다른 글
JVM 메모리 구조 (0) 2023.03.09 [Effective Java] wait와 notify보다는 동시성 유틸리티를 애용하라 (0) 2023.02.14 [Effective Java] 필요 없는 검사 예외 사용은 피하라 (0) 2023.02.06 [Effective Java] 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라 (0) 2023.02.06 [Effective Java] 예외는 진짜 예외 상황에만 사용하라 (0) 2023.02.06