- 병렬 스트림으로 병렬 처리하기
- 병렬 스트림의 성능 분석
- 포크/조인 프레임워크
- Spliterator로 스트림 데이터 쪼개기
🍎병렬 스트림
- parallelStream을 호출하면 병렬 스트림이 생성된다.
- 반대로 sequential을 호출하면 순차 스트림으로 바꾼다. 마지막에 호출한 메서드를 기준으로 병렬인지, 순차인지 판단할 수 있다.
- 병렬 스트림은 내부적으로 ForkJoinPool을 사용한다.
- 병렬화를 이용하려면 스트림을 재귀적으로 분할해야 하고, 각 서브 스트림은 다른 스레드에 할당하여 연산하고, 이들의 결과를 하나로 합쳐야한다. 즉 많은 비용이 발생한다.
스트림 성능 측정
- 성능을 최적화할 때 세 가지 황금 규칙이 있다. 첫째도 측정, 둘째도 측정, 셋째도 측정!!
- jmh 라이브러리를 추가하고, 벤치마크를 사용하여 성능을 측정할 수 있다.
병렬 스트림의 잘못된 사용법
- iterator연산의 경우, 숫자 연산으로 인해 언박싱을 해야하고, 앞의 누적 결과로 연산하기 때문에 청크로 분할하기 어렵다. 따라서 이를 병렬 처리할 경우, 스레드에 할당하는 오버헤드만 증가하여 성능이 더 저하된다.
- LongStream을 사용하면 언박싱 연산이 필요없고, 청크로 분할하기 쉽다.
- 병렬 실행 시 공유된 가변 상태를 사용하면 잘못된 결과가 반환된다.
병렬 스트림 효과적으로 사용하기
- 직접 성능을 측정하라.
- 박싱을 주의하라.기본형 특화 스트림을 사용하라.
- 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산(limit, findFirst)보다는 findAny 연산을 사용하라.
- 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려하라. 하나의 요소를 처리하는데 높은 비용이 든다면, 병렬 스트림으로 성능을 개선할 수 있다.
- 소량의 데이터에서는 병렬 스트림이 도움이 되지 않는다.
- 스트림을 구성하는 자료구조가 적절한지 확인하라. LinkedList보단 ArrayList가 분할하기 더 쉽다.
- 스트림 중간 연산에 따라 분해 과정의 성능이 달라진다. filter연산은 스트림의 결과를 예측할 수 없어서 병렬 처리가 효과적인지 알 수 없다.
- 최종 연산의 병합 비용을 살펴보아라.이 비용이 비싸다면 성능의 이익이 떨어진다.
🍎포크/조인 프레임워크
- 포크/조인 프레임워크에서 제공하는 병렬화 작업의 흐름은 다음과 같다.
- 작업을 재귀적으로 작은 작업으로 쪼갠 다음 그 각각의 결과를 합쳐서 전체 결과를 반환한다.
- ExecutorService 인터페이스를 구현하는 데, 이 인터페이스는 작은 작업(서브태스크)들을 스레드에 분산 할당하는 일을 한다.
RecursiveTask 활용
- 스레드풀을 이용하려면 RecursiveTask의 자식 클래스를 생성해야 한다.
- 클래스 이름만 보아도 알 수 있듯이 재귀적으로 작업을 쪼개는 일을 한다.
- 추상 메서드 compute를 구현해야 한다.
protected abstract R compute();
//compute의 일반적인 로직
if(태스크가 충분히 작거나 더 이상 분할할 수 없으면) {
순차적으로 태스크 계산
} else {
1. 태스크를 두 서브태스크로 분할
2. 태스크가 다시 분할되도록 이 메서드를 재귀 호출
3. 모든 서브태스크의 연산이 완료될 때까지 기다림
4. 두 서브태스크의 결과를 합침.
}
- 일반적으로 어플리케이션에서는 ForkJoinPool을 하나만 사용한다. ForkJoinPool의 인스턴스를 하나만 생성하여 정적필드에 싱글톤으로 사용한다.
ForkJoinSumCalculator(RecursiveTask의 자식) 실행
- ForkJoinSumCalculator를 ForkJoinPool로 전달하면 풀의 스레드가 ForkJoinSumCalculator의 cumpute 메서드를 실행한다.
포크/조인 프레임워크 효율적으로 사용하기
- join 메서드는 두 서브태스크의 작업이 끝날 때까지 블락킹이 발생한다. 그래서 두 서브태스크가 시작된 다음에 join을 호출해야 한다.
- RecursizeTask 내에서는 Fork
- 한 서브태스크에는 fork를, 다른 서브태스크는 compute를 호출하여 한 태스크는 스레드를 재사용한다.
작업 훔치기
- 한 스레드가 지신의 태스크를 다 처리하면 다른 스레드의 큐의 꼬리(tail)에서 작업을 훔쳐와 처리한다.
- 모든 태스크의 작업이 끝날 때까지, 즉 모든 큐의 태스크가 빌 때까지 이 과정을 반복한다.
- 따라서 태스크의 크기를 작게 나누어야 스레드 간의 작업부하를 비슷한 수준으로 유지할 수 있다.
🍎 Spliterator 인터페이스
- 자바 8에서 제공.
- Iterator와 비슷하지만 병렬 작업에 특화되있다.
public interface Spliterator<T> {
// 요소를 순차적으로 소비하면서 탐색할 요소가 남아있으면 true 반환, iterator랑 같음
boolean tryAdvance(Consumer<? super T> action);
//일부 요소를 분할해서 두 번째 Spliterator를 생성
Spliterator<T> trySplit();
// 탐색할 요소의 수 반환
long estimateSize();
int characteristics();
}
댓글