병렬 스트림 이란, 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다.
병렬 스트림을 이용하면 모든 멀티코어 프로세서가 각각의 청크를 처리하도록 할당할 수 있다.
순차 스트림을 병렬 스트림으로 변환하기
순차 스트림에 parallel 메서드를 호출하면 기존의 함수형 리듀싱 연산이 병렬로 처리된다.1
2
3
4
5
6public static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel() // 스트림을 병렬 스트림으로 변환
.reduce(0L, Long::sum);
}
병렬 스트림에서 사용하는 스레드 풀 설정
병렬 스트림은 내부적으로 ForkJoinPool을 사용한다. 기본적으로 ForkJoinPool은 프로세서 수, 즉 Runtime.getRuntime().availableProcessors()가 반환하는 값에 상응하는 스레드를 갖는다.1
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12");
전역 설정 코드로 이후의 모든 병렬 스트림연상에 영향을 준다. 일반적으로 기기의 프로세서 수와 같으므로 특별한 이유가 없다면 ForkJoinPool의 기본값을 그대로 사용할 것을 권장한다.
병렬 스트림 효과적으로 사용하기
- 확신이 서지 않는다면 직접 측정하자.
- 박싱을 주의하자. 자동박싱과 언박싱은 성은을 크게 저하시킬 수 있는 요소다. 기본형 특화 스트림을 사용하자.
- 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산이 있다. limit나 findFirst처럼 요소의 순서에 의존하는 연산을 병렬 스트림에서 수행하면 비싼비용을 치러야 한다.
- 소량의 데이터에서는 병렬 스트림이 도움이 되지 않는다.
스트림의 구성하는 자료구조가 적절한지 확인하라. - 최종 연산의 병합 과정 비용을 살펴보자. 병합 과정의 비용이 비싸다면 병렬 스트림으로 얻은 성능의 이익이 서브스트림의 부분결과를 합치는 과정에서 상쇄될 수 있다.
소스 | 분해성 |
---|---|
ArrayList | 훌륭함 |
LinkedList | 나쁨 |
IntStream.range | 훌륭함 |
Stream.iterate | 나쁨 |
HashSet | 좋음 |
TreeSet | 좋음 |
포크/조인 프레임워크
포크/조인 프레임워크 는 병렬화할 수 있는 작업을 재귀적으로 작은 작업으로 분할한 다음에 서브태스크에 각가의 결과를 합쳐서 전체 결과를 만들도록 설계 되어 있다.
divide-and-conquer 알고리즘의 병렬화 버전이라 생각하면 된다.
RecursiveTask 활용
스레드 풀을 이용하려면 RecursiveTask
RecursiveTask를 정의하려면 추상 메서드 compute를 구현해야 한다.
아래 의사코드와 같이 구현하면 된다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Task extend RecursiveTask<V> {
protected V compute() {
if (분할가능한경우) {
분할;
재귀호출;
합침;
} else {
실제할일
}
}
}
// 호출시..
ForkJoinTask<V> task = new Task();
new ForkJoinPool().invoke(task);
Spliterator
Spliterator는 탐색하려는 데이터를 포함하는 스트림을 어떻게 병렬화할 것인지 정의한다.
자바8은 컬렉션 프레임워크에 포함된 모든 자료구조에 사용할 수 있는 디폴트 Spliterator 구현을 제공하고 있다.
1 | public interface Spliterator<T> { |
Method | Description |
---|---|
tryAdvance | 탐색요소가 남아있으면 참 반환 |
trySplit | 분할하여 Spliterator 생성 |
estimateSize | 탐색해야할 요소 수 |
Reference : Java 8 in Action