Java ‐ 병렬 스트림 - dnwls16071/Backend_Summary GitHub Wiki

📚 Fork/Join 패턴

  • 기본적으로 쓰레드는 한 번에 하나의 작업만을 처리할 수 있다.
  • 따라서 하나의 큰 작업을 여러 쓰레드가 처리할 수 있는 작은 단위의 작업으로 분할해야 한다. 이렇게 분할된 작업을 각각의 쓰레드가 처리하는 것이다.
  • 각 쓰레드의 분할된 작업 처리가 끝나면 분할된 결과를 하나로 모아야 한다.

📚 Fork/Join 프레임워크

  • 자바의 Fork/Join 프레임워크는 자바 7부터 도입된 java.util.concurrent 패키지의 일부로, 멀티코어 프로세서를 효율적으로 활용하기 위한 병렬 처리 프레임워크이다.
  • 분할 정복(Divide and Conquer) 전략
    • 큰 작업들을 작은 단위로 재귀적으로 분할
    • 각 작업 결과를 합쳐 최종 결과를 생성
    • 멀티 코어 환경에서 작업을 효율적으로 분산 처리
  • 작업 훔치기 알고리즘
    • 각 쓰레드는 자신의 작업 큐를 가짐
    • 작업이 없는 쓰레드는 다른 바쁜 쓰레드 큐에서 작업을 훔쳐와서 대신 처리
    • 부하 균형을 자동으로 조절하여 효율성 향상

ForkJoinPool

  • Fork/Join 작업을 실행하는 특수한 ExecutorService 쓰레드 풀
  • 작업 스케줄링 및 쓰레드 관리 담당
  • 기본적으로 사용가능한 프로세서 수만큼 쓰레드를 생성
  • 분할 정복과 작업 훔치기에 특화된 쓰레드 풀

ForkJoinTask

  • Fork/Join 작업의 기본 추상 클래스로 Future를 구현했다.
  • 결과를 반환받는 RecursiveTask 혹은 결과를 반환받지 않는 RecursiveAction을 구현해서 사용한다.
  • 일정 기준을 두고 작업 범위가 작으면 직접 처리하되 크다면 작업을 둘로 분할해 각각 병렬로 처리하도록 구현다.
  • fork() : 현재 쓰레드에서 다른 쓰레드로 작업을 분할해 보내는 동작(비동기 실행)
  • join() : 분할된 작업이 끝날 때까지 기다린 후 결과를 가져오는 동작

📚 자바 병렬 스트림

IntStream.rangeClosed(1, 8)
         .parallel()
         .map(HeavyJob::heavyTask)
         .reduce(0, (a, b) -> a + b);
  • 자바의 병렬 스트림은 Fork/Join 공용 풀을 사용해서 병렬 연산을 수행한다.
  • 직접 쓰레드를 만들 필요없이 스트림에서 parallel() 메서드를 호출하면 스트림이 자동으로 병렬 처리가 된다.

❗병렬 스트림 사용시 주의사항

  • 스트림에 parallel()을 추가하면 병렬 스트림이 된다. 병렬 스트림은 Fork/Join 공용 풀을 사용한다.
  • Fork/Join 공용 풀은 CPU 바운드 작업(계산 집약적인 작업)을 위해 설계되었다. 따라서 쓰레드가 주로 대기해야 하는 I/O바운드 작업에는 적합하지 않다.

[Fork/Join 프레임워크는 CPU 바운드 작업에만 사용]

  • Fork/Join 프레임워크는 주로 CPU 바운드 작업을 처리하기 위해 설계되었다.
  • 이러한 작업은 CPU 사용률이 높고 I/O 대기 시간이 적다.
  • CPU 바운드 작업의 경우 물리적인 CPU 코어와 비슷한 수의 스레드를 사용하는 것이 최적의 성능을 발휘할 수 있다. 쓰레드 코어 수보다 많아지면 컨텍스트 스위칭 비용이 증가하고 쓰레드 간 경쟁으로 인해 성능이 저하될 수 있기 때문이다.

[I/O 작업과 같이 블로킹 대기 시간이 긴 작업을 ForkJoinPool에서 처리하면 다음과 같은 문제가 발생]

  • 쓰레드 블로킹에 따른 CPU 낭비 : ForkJoinPool은 CPU 코어 수에 맞춰 제한된 개수의 쓰레드를 사용한다. I/O 작업으로 쓰레드가 블로킹되면 CPU가 놀게 되어 병렬 처리 효율이 크게 떨어진다.
  • 컨텍스트 스위칭 오버헤드 증가 : I/O 작업 때문에 쓰레드를 늘리면 실제 연산보다 대기 시간이 길어지는 상황이 발생할 수 있다. 쓰레드가 많아질수록 컨텍스트 스위칭 비용도 증가해 오히려 성능이 떨어질 수 있다.
  • 작업 훔치기 무력화 : ForkJoinPool이 제공하는 작업 훔치기 알고리즘은 CPU 바운드 작업에서 빠르게 작업 단위를 계속 처리하도록 설계되었다. 그러나 I/O 대기 시간이 많은 작업은 쓰레드가 I/O로 인해 대기하는 경우가 많아 작업 훔치기가 빛을 발휘하기 어렵고 결국 병렬 처리 장점을 살리기 어렵다.
  • 분할-정복 이점 감소 : Fork/Join 방식을 통해 작업을 잘게 나누어도 I/O 병목이 발생하면 CPU 병렬화 이점이 줄어든다. 오히려 분할된 각각의 작업들이 I/O 대기를 반복하면서 fork(), join() 오버헤드만 증가할 위험이 있다.