[Effective Java 3/E] ITEM 48. 스트림 병렬화는 주의해서 적용하라

    Effective Java 표지. Source : https://blog.insightbook.co.kr/

    *알림 : *
    Effective Java 3판은 Java 9까지 도입된 언어적 기능을 중심으로 서술되어 있습니다. 10버젼 이후의 Java 개발을 하시는 분들은 우회적인 접근법 대신 Java 언어 내 새로 도입된 기능이 더 간결하고 좋을 수 있습니다.

    해당 포스팅은 SSAFY 내 책읽기 스터디의 활동을 통해 작성된 포스팅입니다.
    https://github.com/kjsu0209/JavaBook
    https://medium.com/javabook

     

    JavaBook – Medium

    Documentation space of our book study.

    medium.com

     

    kjsu0209/JavaBook

    책읽기 스터디. Contribute to kjsu0209/JavaBook development by creating an account on GitHub.

    github.com

    Java는 Concurrent programming을 지원하기 위해 Thread, Synchronization, wait/notify, java.util.concurrent, Executor 프레임워크, parallel decom-position, fork-join 패키지 등을 선보였다. 자바 8부터는 parallel 메서드만 호출해도 파이프라인을 병렬실행할 수 있는 스트림을 지원한다.

    Java의 기능과 패키지, 라이브러리 등 도구는 많지만 concurrent programming을 할 때 안전성(safety)와 응답 가능(liveness) 상태를 유지하면서 코딩하는 것은 여전히 어렵다.

    병렬화는 만능이 아니다

    앞의 포스팅에선 다루지 않았지만 ITEM 45에 메르센 소수(2^n-1 꼴로 표현되는 소수, mersenne primes)를 구하는 코드가 있다.

    예제 1 : 메르센 소수를 스트림으로 구하는 코드

    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);
    }

    이 코드는 훌륭히 동작하지만, 스트림 파이프라인에서 parallel()을 호출하면 몇 시간이 지나도 아무것도 출력하지 못한다.

    그 이유는 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 병렬화로 성능 개선을 이룰 수가 없다. limit를 사용할 경우 제한 갯수 이후의 결과를 버려도 Integrity는 깨지지 않음을 가정한다. 매 소수마다 새로이 계산을 하다보니 성능이 끔찍하게 나빠진다.

    병렬화의 이득을 취할 수 있는 스트림 소스의 종류

    • ArrayList
    • HashMap
    • HashSet
    • ConcurrentHashMap
    • Array
    • int range
    • long range

    해당 자료구조들은 데이터를 적당한 크기로 쉽게 나눌 수 있어서 쓰레드에게 일감을 분배하기 좋다. 또한 참조 지역성(locality of reference)이 뛰어나다. 분배는 Spliterator가 담당하며, Stream이나 Iterable의 splliterator 메서드로 객체를 얻어올 수 있다.

    종단 연산과 병렬화

    만약 종단 연산에서 수행하는 작업량이 큰 비중을 차지하면서 순차적이라면 병렬 수행의 효과는 제한된다.

    • reduce
    • min
    • max
    • count
    • sum
    • anyMatch
    • allMatch
    • nonMatch

    위 메서드들은 병렬화에 적합한데, 원소 처리 순서에 따른 결과가 달라지지 않고 일감을 나누기에 좋거나(reduce), 조건에 맞으면 바로 반환되는 메서드(anyMatch 등)도 일감을 나누기 좋다.

    반면에 collect 메서드는 가변 축소(mutable reduction)가 이뤄지기 때문에 병렬화에 적합하지 않다.

    참조하기 : Java 8 Streams - collect vs reduce

    안전 실패(Safety Failure)

    스트림의 성능 뿐 아니라 mappers, filters나 직접 정의한 함수 객체의 문제 때문에 병렬화가 잘못된 결과를 가져올 수 있다. Stream의 reduce 연산에 쓰이는 accumulator와 combiner는

    1. associative : 결합법칙을 만족함
    2. non-interfering : 파이프라인 외부에서 소스를 변경하지 않는다.
    3. stateless

    해야한다. 그렇지 않다면 병렬화에서 올바른 답을 내는 것을 보장할 수 없다.7

    댓글