HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🛁
공부기록
/
📚
책 정리
/
🌑
Item48 - 스트림 병령화는 주의해서 사용하라
🌑

Item48 - 스트림 병령화는 주의해서 사용하라

속성
7장
OverView안정성과 응답가능 상태 유지병렬화 하기 좋은 경우마무리REFER

OverView

  • 스트림과 반복자의 차이
    • 🤔
      비슷한 역할의 반복자이만, 람다식으로 코드처리를 할 수 있다는 점과 병렬 처리가 쉽다는 점이 있으며 중간 처리, 최종 처리와 같은 작업의 분기를 나눌수 있는 점에서 차이가 난다.
      • 중간 처리 - 매핑, 필터링, 정렬 수행
      • 최종 처리 - 반복, 카운팅, 평균, 총합
내부/외부 반복자 이야기

내부 반복자를 사용하므로 병렬처리가 쉽다.

  • 외부 반복자 : 개발자가 코드로 직접 컬렉션의 요소를 가져오는 패턴 (index를 활용한 for문, iterator, while문 등)
  • 내부 반복자 : 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야할 코드만 제공하는 패턴
notion image
  • 스트림은 일련의 파이프 라인으로 동작한다.
    • ⚠️중간에 끊어질 수 없게 되어있음.
  • 그러한 특징 때문에 stream Lazy한 특성을 가지고 있다.(해당 관련해서는 꼭 찾아보돍 …)
    • notion image
 
 

안정성과 응답가능 상태 유지

동시성 프로그래밍을 할 때는 안정성(safety)과 응답 가능(liveness) 상태를 유지하기 위해 노력해야하는데, 병렬 스트림 파이프라인 프로그래밍에서도 동일하다.
다음 예는 스트림을 사용해 20개의 메르센 소수를 생성하는 프로그램이다.
public static void main(String[] args) { primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) .filter(mersenne -> mersenne.isProbablePrime(50)) .limit(20) .forEach(System.out::println); } static Stream<BigInteger> primes() { return Stream.iterate(TWO, BigInteger::nextProbablePrime); }
프로그램 수행시 약 12.5초 정도 걸리는데, 속도를 높이고 싶어 paralle()을 호출하면, 아무것도 출력하지 못하면서 CPU는 90%나 차지하는 상태가 되어, 강제 종료시까지 응답없는 상태가 될 수 있다.
이러한 현상은 스트림 라이브러리가 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문에 발생한 것이다. 데이터 소스가 Stream.iterate() 이거나 중간 연산으로 limit()을 사용하면 파이프라인 병렬화로는 성능 개선을 할 수 없다. 즉, 스트림 파이프라인을 마구잡이로 병렬화하면 안되며, 오히려 성능이 나빠질 수 있다.
🪂
Parallel Stream 성능장애를 조심하라❗

병렬화 하기 좋은 경우

 
참조 지역성이 뛰어난 경우
- `ArrayList` - `HashMap` - `HashSet` - `ConcurrentHashMap` - 배열 - int 범위 - long 범위
  • 위 자료구조들은 모두 데이터를 원하는 크기로 정확하고 쉽게 나눌 수 있어, 일을 다수의 스레드에 분배하기 좋다.
  • 원소들을 순차적으로 실행할 때 참조 지역성이 뛰어나다.
    • (참조지역성 : 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있음.)
  • 참조 지역성이 낮으면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리며 대부분 시간을 낭비하며 보내게 되며, 참조 지역성은 대량의 데이터를 처리하는 벌크 연산을 병렬화 할 때 아주 중요한 요소로 작용한다. 기본 타입의 배열은 데이터 자체가 메모리에 연속해서 저장되기 때문에 참조 지역성이 가장 뛰어나 병렬화 효과가 가장 좋다.
 
종단 연산 - 축소(reduction) 고려해서 잘 사용하기
  • 종단 연산에서 수행하는 작업량이 파이프라인 전체 작업에서 상당 비중으로 차지하며, 순차적인 연산이라면 파이프라인 병렬 수행의 효과는 제한될 수 밖에 없다.
  • 축소(reduction)는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업이다. reduce 메서드 min, max, count, sum 완성된 형태로 제공되는 메서드 anyMatch, allMatch, noneMatch 와 같이 조건에 맞으면 바로 반환하는 메서드 위 메서드는 병렬화에 적합하지만, 가변 축소를 수행하는 Stream의 collect 메서드는 컬렉션들을 합치는 부담이 크기때문에 병렬화에 적합하지 않다.
    • 축소 예시
      package com.programmers.java.chapt7.item48; import java.util.List; import java.util.stream.IntStream; public class Main { public static void main(String[] args) { Main main = new Main(); List<Integer> numbers = main.generateRandomNumbers(1, 4); int initValue = 5; Integer sumByParallel = numbers.parallelStream().reduce(initValue, Integer::sum); Integer sumBySequential = numbers.stream().reduce(initValue, Integer::sum); /** * note : 원래 1-4 까지의 합은 10이다. 그리고 거기에 초기값을 더해 15가 나와야 한다... */ System.out.println("sumByParallel = " + sumByParallel); System.out.println("sumBySequential = " + sumBySequential); System.out.println("병렬 스레드 naming 출력"); numbers.parallelStream() .forEach(val -> System.out.println("thread name : " + Thread.currentThread().getName())); } public List<Integer> generateRandomNumbers(int start, int end) { return IntStream.rangeClosed(1, 10) .boxed() .toList(); } }
       
      • 실행 결과
      🤔
      결과적으로 서로 다른 값이 나오게 된다. (이런 경우면 병렬을 쓰면 안되겟ㄸㄸㄸ
      notion image
      • 축소 작업이 병렬로 처리되었기 때문에 이런 문제가 발생하는 것이다.
        • (5+1) + (5+2) + (5+3) + (5+4) = 30
      해당 원인은 무엇인지 한번 고민해보세요 ❗
      ✅
      원인은 종단 함수에서 함수가 상태를 가졌기 때문입니다! 여기서의 상태는 초기값(변수명 : initValue)을 의미하고 있습니다.
      • initValuye=0으로 하면 완벽해집니다.
      notion image
  • spliterator 메서드 재정의 직접 구현한 Stream, Iterable, Collection이 병렬화 이점을 제대로 누리게 하려면 spliterator 메서드를 반드시 재정의하고 결과 스트림의 병렬화 성능을 강도 높게 테스트하는 것이 좋다. 하지만, spliterator 메서드를 재정의 하는 것은 난이도가 있으니.. 잘 알아야 한다..
    • spliterattor
      • 분할 할 수 있는 반복라 라는 의미로 병렬작업에 특화되어 있는 키워드 이다.
      • 커스텀 하게 한다면 기존 제공되어지는 spliterator 보다 더 성능 향상을 기대할 수 있다.
      public interface Spliterator<T> { boolean tryAdvance(Consumer<? super T> action); Spliterator<T> trySplit(); long estimateSize(); int characteristics(); ... }
      여기서 T는 Spliterator에서 탐색하는 요소의 형식을 가리킨다.
      • tryAdvance : 요소를 하나씩 소비하면서 탐색해야 할 요소가 남아있으면 true 반환
      • trySplit : 일부 요소를 분할해서 두 번째 Spliterator를 생성
      • estimateSize : 탐색해야 할 요소의 수 제공
      • characteristics : Spliterator 객체에 포함된 모든 특성값의 합을 반환각 특성은 어떤 Spliterator 객체인가에 따라 다르며 그에 따른 각 메서드들의 내부적인 동작이 다를 수 있다.
      • 스플릿 분할 과정
        • notion image
          Step1. Spliterator에 trySplit를 호출해서 두 번째 Spliterator가 생성된다.
          Step2. 두 개의 Spliterator에서 trySplit를 호출해 총 네개의 Spliterator가 생성된다.
          Step3. trySplit이 null을 반환하면 더 이상 분할할 수 없다.
          Step4. 모든 trySplit이 null을 반환하면 재귀 분할 과정이 종료 된다.

마무리

💡
스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작(safety failure)이 발생할 수 있다.
Stream 명세대로 동작하지 않을 때, 발생할 수 있으며 를들어, Stream reduce 연산의 accumulator와 combiner 함수는 반드시 결합 법칙을 만족하고, 간섭받지 않고, 상태를 갖지 않아야한다.
위 조건을 다 만족하더라도, 병렬화에 드는 추가 비용을 상쇄하지 못한다면, 성능 향상이 미미할 수 있으며 스트림 안의 원소 수와 원소당 수행되는 코드 줄 수를 곱해 수십만이 되어야 성능향상을 느낄 수 있다.
 
스트림 병렬화는 오직 성능 최적화 수단이다. 변경 전후로 테스트해 병렬화 사용에 가치가 있는지 확인해야한다.
계산이 정확하고, 확실히 성능이 좋아졌을 경우에만 병렬화를 실 운영에 적용해야한다.
조건이 잘 갖춰지면, parallel 메서드 호출 하나로 프로세서 코어 수에 비례하는 성능 향상을 만끽할 수 있다.
 
 

REFER

[모던 자바] Spliterator 인터페이스란 무엇인가?
Iterator 처럼 Spliterator는 소스의 요소 탐색 기능을 제공한다는 점은 같지만 Spliterator는 병렬 작업 에 특화되어 있다. 커스텀 Spliterator를 꼭 구현해야 하는 건 아니지만 Spliterator가 어떻게 동작하는지 이해한다면 병렬 스트림 동작 과 관련한 통찰력을 얻을 수 있다. java8은 컬렉션 프레임워크에 포함된 모든 자료구조에 사용할 수 있는 디폴트 Spliterator 구현을 제공한다.
[모던 자바] Spliterator 인터페이스란 무엇인가?
https://devbksheen.tistory.com/entry/%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94-Spliterator-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
[모던 자바] Spliterator 인터페이스란 무엇인가?
[Java] Stream과 병렬처리 - 1
스트림(Stream)은 Java 8 버전 이후부터 추가되었으며, Collection과 배열의 저장요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 도와주는 반복자 Java 7 이전에는 Collection의 저장 요소를 참조하기 위해서 Iterator를 사용하였다. Java 8 이후에는 Stream을 사용해서 저장 요소를 하나씩 참조가 가능하며, forEach() 메소드와 같은 Consumer 함수 인터페이스를 이용해서 람다식으로 사용이 가능하다.
[Java] Stream과 병렬처리 - 1
https://velog.io/@dev_jhjhj/Java-Stream%EA%B3%BC-%EB%B3%91%EB%A0%AC%EC%B2%98%EB%A6%AC-1
[Java] Stream과 병렬처리 - 1
자바8 Streams API 를 다룰때 실수하기 쉬운것 10가지
이 글은 자바 8 Stream API 를 아는 사람이 주의해야 할 것에 대해 쓰여진 글이지만 , 몰라도 상관없습니다. 이 글 읽어보면 대충 이런거구나 알 수 있으니깐요. Java 8 Stream API 을 배워야하는 이유로 "가독성/간편성" 과 "성능/공짜점심" 으로 보통 꼽습니다. 여러줄의 코드가 한줄로 되어버렸습니다. 가독성이 좋아졌고, 실수할 여지를 줄여 놓았습니다. 라고 광고합니다.
자바8 Streams API 를 다룰때 실수하기 쉬운것 10가지
https://hamait.tistory.com/547
자바8 Streams API 를 다룰때 실수하기 쉬운것 10가지
java.util.stream (Java Platform SE 8 )
Interface Description Base interface for streams, which are sequences of elements supporting sequential and parallel aggregate operations. A mutable reduction operation that accumulates input elements into a mutable result container, optionally transforming the accumulated result into a final representation after all input elements have been processed.
java.util.stream (Java Platform SE 8 )
https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#:~:text=For%20parallel%20streams,under%20ordering%20constraints