Skip to content

Latest commit

 

History

History
138 lines (116 loc) · 6.97 KB

4. ThreadPool.md

File metadata and controls

138 lines (116 loc) · 6.97 KB

ThreadPoolExecutor (java.util.concurrent)

  • java.util.concurrent 패키지에는 ThreadPoolExecutor라는 이름의 범용 스레드 풀 구현체가 추가되었습니다.
  • 해당 패키지 이전에는 모든 서블릿 컨테이너가 독자적인 스레드 풀을 구현해 사용했지만,
    현재 대부분의 구현체는 ThreadPoolExecutor 구현체로 스레드 풀 구현을 교체하거나
    최소한 ThreadPoolExecutor와 인터페이스를 맞춘 구현체를 제공합니다.
  • 이 ThreadPoolExecutor 스레드 풀 구현체가 기존의 스레드 풀에 비해 월등한 성능을 가졌다고 입증되어 그렇다기보다는
    언어 자체에서 제공하는 표준 구현체를 사용하는 것이 서블릿 컨테이너를 구현하는 사람들 사이에서도
    추가 커뮤니케이션 비용을 줄일 수 있기 때문이라고 할 수 있습니다.

코드 예제

  • 아래 코드는 1번부터 13번까지 차례대로 출력되는 것을 확인할 수 있습니다.
public class WalkSerial {

    private Object lock = new Object();
    
    private void printMessage(int index) {
        synchronized (lock) {
            try {
                lock.wait(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%d번째 MinGOOD 입장\n", index);
        }
    }
    
    private void action(int size) {
        System.out.printf("%d명의 MinGOOD 분신술\n", size);
        for (int i = 0; i < size; ++i) {
            printMessage(i+1);
        }
    }
    
    public static void main(String[] args) {
        WalkSerial ws = new WalkSerial();
        System.out.println("차례대로 출력되는 예제");
        ws.action(10);
    }
    
}
  • 아래코드는 ThreadPoolExecutor 예제입니다.
package com.study.threadpool;

class People implements Runnable {
    private int index;
    private Object lock = new Object();
    
    public People(int index) {
        this.index = index;
    }
    
    @Override
    public void run() {
        synchronized (lock) {
            try {
                lock.wait(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%d번째 MinGOOD 입장\n", index);
        }
    }
}
package com.study.threadpool;

import java.util.concurrent.*;

public class WalkParallel {

    private void action(int size) {
        System.out.printf("%d명의 MinGOOdD 분신술\n", size);
        
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        for (int i = 0; i < size; ++i) {
            threadPoolExecutor.execute(new People(i + 1));
        }
        threadPoolExecutor.shutdown();
    }

    public static void main(String[] args) {
        WalkParallel wp = new WalkParallel();
        System.out.println("ThreadPoolExecutor 예제");
        wp.action(10);
    }
    
}
  • 전체 수행은 2초 내로 종료되며, 순서에 상관없이 무작위로 나타납니다.
  • 또한, 스레드 풀은 내부에 요청이 들어왔을 때 처리하기 위해 대기 상태의 스레드를 계속 유지하므로
    현재 작업 큐가 비었다고 자동으로 종료되지 않습니다.
  • 위 예제에서는 shutdown 메소드를 호출해 스레드 풀 자체를 종료시켰습니다.
  • 서블릿 컨테이너는 ThreadPoolExecutor의 LifeCycle을 적절히 관리해주어야 합니다.

ThreadPoolExecutor 사용

  • ThreadPoolExecutor의 사용은 비교적 직관적으로 이해할 수 있습니다.
  • 병렬로 수행할 작업을 Runnable 타입으로 생성해 ThreadPoolExecutor의 executor 메소드 인자로 넘겨주기만 하면 됩니다.
  • 여기서 주의할 점은 작업 큐로 사용할 BlockingQueue의 성질에 따라 미묘하게 3가지 다른 수행 방식이 있다는 것입니다.

예제에서 사용한 SynchronousQueue는 수행 요청이 들어오면

  • 요청을 쌓아두지 않고 풀 내에 준비된 스레드를 즉시 매칭하여 처리합니다.
  • 따라서 스레드 풀 내에 준비된 스레드가 모두 다른 일을 수행한다면,
    최대 가능 스레드 수가 초과되지 않는 한 즉시 새 스레드를 생성해 요청을 처리합니다.
  • 최대 가능 스레드 이상의 요청이 들어오면 해당 요청은 버려지므로
    최대 가능 스레드 수는 제한이 없게 크게 설정하는 것이 일반적입니다.
  • 따라서, 처리되는 속도보다 요청이 들어오는 속도가 더 빠르면 스레드의 수가 비정상적으로 증가할 수 있습니다.

나머지 두 방식은
최대 가능 스레드 수에 제한 있는 큐를 사용하는 방식과 제한 없는 큐를 사용하는 방식입니다.

  • 이 두 방식에는 실질적으로 최대 가능 스레드 수는 의미가 없습니다.
  • 왜냐하면, ThreadPoolExecutor 생성자의 첫 번째 인자인 corePoolSize 수만큼만 스레드를 사용해 동작하며,
    이상의 요청이 들어오면 스레드의 수를 늘리는 대신 추가 요청을 작업 큐에 쌓아 놓고 처리합니다.
  • 앞의 경우와 같이 처리되는 속도보다 요청이 들어오는 속도가 더 빠를 경우
    제한 없는 큐(LinkedBlockingQueue)는 큐의 크기가 비정상적으로 커지는 문제가 발생할 수 있습니다.
  • 반면, 제한 있는 큐(ArrayBlockingQueue)는 미리 정해진 큐 사이즈를 넘는 요청을 모두 버립니다.
  • 따라서, 이 세 타입은 모두 서로 기능 및 안정성에 대한 Trade-Off가 있으며,
    어떤 타입이 언제나 옳은 것이 아니라 사용되는 상황에 맞게 적절히 선택해야 합니다.

적정 병렬 진행 수

  • 대부분의 서블릿 컨테이너에는 서블릿을 동작시키는 스레드의 숫자를 설정할 수 있습니다.
  • 즉, 서블릿을 처리하기 위해 스레드 풀이 내부적으로 유지하는 Walker 스레드의 숫자를 지정할 수 있으며
    이 값을 어떻게 설정하느냐에 따라 서블릿 컨테이너의 전체 처리량이 크게 영향을 받습니다.

설정치를 구하기 위해 고려해야 할 사항

  • 컴퓨터의 연산 처리는 CPU가 수행합니다. 즉, CPU가 가진 core의 수만큼 계산 작업을 동시에 처리할 수 있습니다.
  • 이 말은 서블릿 컨테이너가 CPU 코어 수 미만의 스레드를 사용한다면
    1개 이상의 CPU 코어는 서블릿 컨테이너가 사용하지 못한다는 의미입니다.
  • 따라서 최대 스레드 수 설정은 CPU 코어 수보다 작지 않아야 합니다.
  • 또한, 병렬 진행되어야 하는 스레드의 수가 CPU 코어 수보다 큰 경우,
    모든 스레드가 동시에 CPU 코어를 사용할 수는 없습니다.
  • 다시 말해, 다수의 스레드가 소수의 CPU 코어를 이용하려면,
    특정 시간에는 반드시 하나 이상의 스레드가 CPU 코어를 점유하지 않고 대기 상태에 있어야 합니다.