시냅스

Java8 Parallel Stream 과 성능, 동시성 문제에 대해 본문

Java, Spring

Java8 Parallel Stream 과 성능, 동시성 문제에 대해

ted k 2023. 3. 25. 22:05

https://javatechnocampus.wordpress.com/2015/10/03/544/

 

Parallel Stream

  • Stream api 의 일종
  • 컬렉션, 배열, 파일 등의 데이터 소스를 처리할 수 있다.
  • 스트림을 병렬 처리하여 처리 속도를 높이는 효과가 있다.
    • 내부적으로 배열을 가지고 있고, 배열을 쪼개어 각 스레드가 처리하게 된다.
  • 요소들이 여러 개의 스레드에 분산되어 병렬 처리된다.
    • 분할 정복과 비슷하다.
  • 이때 stream api 에서는 스레드 풀을 사용하여 스레드의 생성 및 관리를 담당한다.
  • 내부적으로 fork/join 프레임 워크를 사용하여 요소들을 분할하고 각각의 스레드에서 병렬 처리한다.
  • 각각의 작업은 스레드 풀의 작업 큐에 추가되고 스레드 풀은 작업 큐에서 작업을 가져와 스레드에 할당한다
// Parallel Stream을 사용한 합계 계산
int sumParallel = Arrays.stream(arr).parallel().sum();

 

Parallel Stream 이 사용하는 thread pool

  • 기본 스레드 풀은 ForkJoinPool.commonPool
  • 이 스레드 풀은 기본적으로 시스템의 프로세서 수에 따라 스레드 수를 동적을 조정한다.
    • Runtime.availableProcessors 만큼
    • 직접 조정할 수도 있지만, 병렬성의 입장에서 추천하지 않는다.
  • 다만 ForkJoinPool.commonPool 은 다른 작업에서도 사용되므로 parallel stream 에서 사용할 때는 일부 상황에서 성능 이슈가 발생할 수 있다.
    • 함께 사용하는 ForkJoinPool.commonPool 의 작업 큐에 현재 parallel 관련 작업이 아니라 다른 작업이 있을 수도 있기 때문에
      • CompletableFuture class 와 같은 비동기 작업
      • RecursiveAction, RecursiveTask 와 같이 compute 를 사용하는 작업
    • 각 스레드끼리는 만약 스레드 B 에 작업이 몰려있고, A 에 작업이 없다면, A 는 B 의 작업을 가져오며 최적의 성능을 낸다
    • 다만 링크드 리스트와 같이 사이즈를 정확히 알기 어려운 자료구조는 분할되지 않아 순차처리하므로 효과를 보기 어렵다
      • 물론 size() 를 통해 알 수 있지만, size 를 바꾸는 일은 스레드 간 race condition 을 일으켜 hot spot 이 된다.
      • 따라서 분할정복을 적용하기 어렵다.
    • 위와 마찬가지로 연산 중간에 변수를 공유해야 되는 작업 (sort, distinct) 들은 내부적으로 상태 변수에 대해 공유(synchronized)해야 하므로 순차적으로 적용하는 것이 보다 효과적일 수 있다.
    • 고로 분할이 잘 이루어질 수 있는 데이터 구조이거나, 작업이 독립적이면서 CPU사용이 높은 작업에 적합하다.
  • 다른 작업 때문에 성능 문제가 우려될 경우 ForkJoinPoll 인스턴스를 만들고 해당 인스턴스를 parallel stream 에 명시적으로 제공하여 사용자 정의 스레드 풀을 만들 수 있다.
// 4개의 스레드를 사용하는 사용자 정의 스레드 풀 생성
ForkJoinPool customThreadPool = new ForkJoinPool(4); 
IntStream.range(0, 100).parallel().forEach(i -> {
    // 사용자 정의 스레드 풀을 사용하여 parallel stream 실행
}, customThreadPool);
Comments