[Java] 병렬 스트림(Parallel Stream)이란?
1. Question
병렬 스트림(Parallel Stream)
이란?
2. Answer
병렬 스트림(Parallel Stream)
은 Java에서 데이터를 병렬로 처리하여 성능을 향상시킬 수 있는 강력한 기능이다. 여러 CPU 코어를 활용하여 작업을 동시에 실행함으로써, 특히 대용량 데이터 처리에 있어서 시간을 절약할 수 있다. 그러나, 병렬 스트림의 사용은 적절한 상황에서 이루어져야 하며, 잘못 사용되었을 때는 오히려 성능 저하를 가져올 수 있다. 다음 코드에서와 같이, 순차 스트림에 parallel
메서드를 호출하면 기존의 함수형 리듀싱 연산(숫자 합계 계산)이 병렬로 처리된다.
public long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel() // 스트림을 병렬 스트림으로 변환
.reduce(0L, Long::sum);
}
3. Detail
A. 내부 작동 원리
-
Fork/Join Framework
: 병렬 스트림은Java 7
에 도입된Fork/Join Framework
를 기반으로 한다. 이 프레임워크는 큰 작업을 작은 작업으로 분할하고, 각각의 작업을 별도의 스레드에서 처리한 다음, 결과를 결합하는 방식으로 동작한다. -
작업 분할: 데이터 소스(예: 컬렉션, 배열 등)는 여러 청크로 분할된다. 이는 병렬 처리의 기본 단위가 되며, 각 청크는 별도의 스레드에서 처리된다.
-
병렬 실행: 각 청크는 Java의 스레드 풀에서 관리되는 스레드에서 병렬로 처리된다. CPU 코어의 수에 따라, 동시에 실행될 수 있는 스레드의 수가 결정된다.
-
결과 결합: 모든 청크의 처리가 완료되면, 각 청크의 결과는 최종 결과로 결합된다. 결합 과정은 연산의 종류(예:
reduce
,collect
)에 따라 다를 수 있다.
B. 사용 방법
- 컬렉션의
parallelStream()
메서드를 호출하거나, 일반 스트림에 대해parallel()
메서드를 호출하여 병렬 스트림을 얻을 수 있다.
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> parallelStream = list.parallelStream();
- 병렬 스트림에서는
map
,filter
,reduce
,collect
등의 연산을 사용할 수 있으며, 이러한 연산은 병렬로 실행된다.
int sum = list.parallelStream().mapToInt(Integer::parseInt).sum();
C. 사용 시 주의사항
-
스레드 안정성: 병렬 스트림에서는 여러 스레드가 데이터에 동시에 액세스할 수 있으므로, 연산이 스레드 안전해야 한다. 특히, 상태를 공유하는 객체에 대한 접근은 동기화되거나 스레드 안전한 방법으로 처리되어야 한다.
-
연산의 병렬화 적합성: 모든 작업이 병렬 처리에 적합한 것은 아니다. 작업이 CPU 바운드이고, 데이터가 충분히 크며, 작업 간에 의존성이 없는 경우에 병렬 스트림이 성능 향상을 가져올 수 있다.
-
오버헤드 고려: 작업을 분할하고, 스레드에 할당하며, 결과를 결합하는 데는 오버헤드가 발생한다. 데이터 크기가 작거나 연산이 간단한 경우에는 병렬 스트림을 사용하는 것이 오히려 성능을 저하시킬 수 있다.
-
순서 보장: 병렬 스트림에서는 요소 처리 순서가 보장되지 않는다. 순서가 중요한 작업에는 병렬 스트림의 사용을 신중히 고려해야 한다.
-
컬렉션 선택: 데이터 소스의 종류에 따라 병렬 처리의 효율성이 달라질 수 있다. 예를 들어,
ArrayList
는 접근이 빠르므로 병렬 처리에 적합하지만,LinkedList
는 요소 접근 시간이 길어 병렬 처리에는 적합하지 않다. -
직접 측정: 순차 스트림을 병렬 스트림으로 쉽게 바꿀 수 있지만, 무조건 병렬 스트림으로 바꾸는 것이 항상 좋은 선택은 아니다. 병렬 스트림과 순차 스트림 중 어느 것이 더 나은 성능을 제공하는지 확신이 서지 않는다면, 적절한 벤치마크를 사용하여 직접 성능을 측정하는 것이 바람직하다.
-
박싱 주의: 자동 박싱과 언박싱은 성능을 크게 저하시킬 수 있다.
Java 8
은 박싱 동작을 피할 수 있도록 기본형 특화 스트림(IntStream
,LongStream
,DoubleStream
)을 제공하므로, 가능한 한 이러한 스트림을 사용하는 것이 좋다. -
최종 연산의 병합 과정 비용 고려: 최종 연산에서 여러 서브스트림의 결과를 병합하는 과정, 예를 들어
Collector
의combiner
메서드에 의한 비용이 높다면, 병렬 스트림으로 얻은 성능 이익이 이 병합 과정에서 상쇄될 수 있다. 병렬 스트림을 사용하기 전에는 이러한 비용도 고려해야 한다.
ArrayList
,IntStream.range
=> 분해성 ‘훌륭함’HashSet
,TreeSet
=> 분해성 ‘좋음’LinkedList
,Stream.iterate
=> 분해성 ‘나쁨’
4. Reference
- “모던 자바 인 액션” (저자: 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트)
댓글남기기