[교재 EffectiveJava] 아이템 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라

반응형
728x90
반응형

들어가기전

아이템 80에서 나오는 '실행자 프레임워크'에 대해 아래의 포스팅으로 공부하자.

https://devfunny.tistory.com/807?category=957918 

 

[JAVA8 병렬프로그래밍] Executors 클래스, ExecutorService 인터페이스

Executors 클래스 - Executor 인터페이스 : 컨커런트 API의 핵심 인터페이스다. 이 인터페이스를 구현한 여러 종류의 클래스를 기본으로 제공한다. - 스레드 풀 : 스레드를 관리하기 위한 풀이다. 병렬

devfunny.tistory.com

 

 

java.util.concurrent 패키지의 등장

java.util.concurrent 패키지는 실행자 프레임워크(Executor Framework)라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고있다. 다음의 단 한줄로 작업 큐를 생성할 수 있다. 

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newSingleThreadExecutor();
    }
}

이 실행자에게 실행할 태스크(task; 작업)를 넘기는 방법이다.

exec.execute(runnable);

실행자를 종료시켜보자. 

exec.shutdown();

 

 

실행자 서비스의 주요 기능

  • 특정 태스크가 완료되기를 기다린다.
  • 태스크 모음 중 아무것 하나(invokeAny 메서드) 혹은 모든 태스크(invokeAll 메서드)가 완료되기를 기다린다.
  • 실행자 서비스가 종료하기를 기다린다(awaitTermination 메서드).
  • 완료된 태스크들의 결과를 차례로 받는다(ExecutorCompletionService 이용).
  • 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다(ScheduledThreadpoolExecutor 이용).

 

 

java.util.concurrent.Executors

큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩터리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다. 스레드 풀의 스레드 개수는 고정할 수도 있고 필요에 따라 늘어나거나 줄어들게 설정할 수도 있다.

 

필요한 실행자 대부분은 java.util.concurrent.Executors의 정적 팩터리들을 이용하여 생성할 수 있다.

 

Executors.java
public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
    
    ...

    /** Cannot instantiate. */
    private Executors() {}
}

 

 

Executors.newCachedThreadPool

특별히 설정할게 없고 일반적인 용도에 적합하다. 무거운 프로뎍선 서버에는 좋지 못하다.
CachedThreadPool에서는 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행된다. 가용한 스레드가 없다면 새로 하나를 생성한다.
서버가 아주 무겁다면 CPU 이용률이 100%로 치닫고, 새로운 태스크가 도착하는 족족 또 다른 스레드를 생성하며 상황을 더욱 악화시킨다.

따라서 무거운 프로덕션 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나 완전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 낫다.

https://devfunny.tistory.com/807?category=957918 참고

메서드 설명
newSingleThreadExecutor - 오직 하나의 스레드로 처리하며 나머지 스레드 생성 요청은 현재 스레드가 종료될때까지 대기한다. 
- 현재 메인 클래스에서 오직 하나의 스레드로 작업을 수행할 때 안전하게 사용할 수 있으나, 여러개의 스레드를 생성할 수 없다.
newFixedThreadPool - 입력 파라미터로 생성할 스레드 풀의 크기를 정의한다. 스레드 풀의 크기 내에서 스레드가 생성되어 병렬 처리된다.
- 스레드 풀의 크기를 넘으면 풀에 여유가 생길때까지 대기한다.
newCachedThreadPool - 멀티 스레드 처리를 위한 스레드 풀을 생성하되 기존에 생성한 스레드를 가능한한 재사용한다.
- 멀티 스레드 기반으로 동작한다는 점에서 newFixedThreadpool과 동일하지만, 등록한 스레드를 모두 한번에 실행시키며 동시 처리에 대한 개수 제한이 없다.
newWorkStealingPool - 스레드 풀을 생성하며, 실행되는 하드웨어의 사용 가능한 모든 프로세스(CPU)의 코어를 쓰도록 병럴 처리 레벨을 설정한다.
- 해당 하드웨어의 자원을 모두 선점하려고 하기 때문에 다른 프로세스 혹은 애플리케이션 성능에 영향을 끼친다.
unconfigurableExecutorService - 메서드의 입력 파라미터로 반드시 ExecutorService 객체를 전달해야한다. 그리고 해당 객체를 표준 ExecutorService 객체로 위임해서 결과를 리턴한다.
- ExecutorService를 구현한 여러 클래스의 기능 중 ExecutorService의 메서드만 호출하고 나머지 기능을 사용하지 못하도록 제한할 필요가 있을때 사용한다.

 

 

태스크

작업 큐를 손수 만드는 일은 삼가야하고, 스레드를 직접 다루는 것도 일반적으로 삼가야한다. 실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다. 작업 단위를 나타내는 핵심 추상 개념이 태스크다.

 

태스크의 두가지 

  • 1) Runnable
  • 2) Callable(값을 반환하고 임의의 예외를 던질 수 있다.)

 

태스크 수행을 실행자 서비스에 맡기면 원하는 태스크 수행 정책을 선택할 수 있고, 생각이 바뀌면 언제든 변경할 수 있다. 핵심은 실행자 프레임워크가 작업 수행을 담당해준다는 것이다.

 

 

포크-조인 프레임워크

https://devfunny.tistory.com/808?category=957918 

 

[JAVA8 병렬프로그래밍] 포크/조인 프레임워크

포크/조인 프레임워크 컨커런트 API를 최종적으로 완성한 것은 자바 7에서 제공한 포크/조인 프레임워크다. 포크/조인 프레임워크는 java.util.concurrent 패키지의 핵심인 ExecutorService 인터페이스를

devfunny.tistory.com

자바 7이 되면서 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원하도록 확장되었다. 포크-조인 태스크는 포크-조인 풀이라는 특별한 실행자 서비스가 실행해준다.

포크-조인 태스크, 즉 ForkJoinTask의 인스턴스는 작은 하위 태스크로 나뉠 수 있고, ForkJoinPool이 구성하는 스레드들이 이 태스크들을 처리하며, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수 있다. 

 

이렇게하여 모든 스레드가 바쁘게 움직여 CPU를 최대한 활용하면서 높은 처리량과 낮은 지연시간을 달성한다. 포크-조인 풀을 이용해 만든 병렬 스트림을 이용하면 적은 노력으로 그 이점을 얻을 수 있다. 

 

병렬 스트림 : 아이템 48 참고

https://devfunny.tistory.com/606?category=895441 

 

[교재 EffectiveJava] 아이템 48. 스트림 병렬화는 주의해서 적용하라

파이프라인 병렬 실행 자바 8부터는 parallel 메서드만 한번 호출하면 파이프라인을 병렬 실행할 수 있는 스트림을 지원했다. 이를 올바르고 빠르게 작성하는 일은 여전히 어려운 작업이다. 동시

devfunny.tistory.com

 

 

 

반응형

Designed by JB FACTORY