Thread Pool(쓰레드 풀)이란?? - swsuh93/study GitHub Wiki
쓰레드 풀은 간단히 쓰레드를 미리 만들어 놓은 하나의 풀이다.
쓰레드라는 것이 생성될 때 컴퓨터 내부적으로 운영체제(OS)가 요청을 받아들여 메모리 공간을 확보해주고 그 메모리를 쓰레드에게 할당해준다. 쓰레드는 동일한 메모리 영역에서 생성되고 관리되지만, 생성, 수거에 드는 비용을 무시할 수 없다
그렇기 때문에 요청이 들어올 때 마다 쓰레드를 생성하고 일을 다하면 수거하고 하는 작업은 프로그램 퍼포먼스에 지대한 영향을 줄 수 있다.
따라서 쓰레드를 미리 만들어 놓는 것이다.
Thread Pool의 동작 원리
우리가 만든 어플리케이션에서 사용자로부터 들어온 요청을 작업 큐에 넣고 쓰레드 풀은 작업 큐에 들어온 일감을 미리 생성해 놓은 쓰레드들에게 할당한다.
일을 다 처리한 쓰레드들은 다시 어플리케이션에게 결과값을 리턴한다.
Thread Pool을 왜 사용할까??
- 프로그램 성능 저하를 방지하기 위해 매번 발생되는 작업을 병렬처리하기 위해 쓰레드를 생성/수거하는데 따른 부담은 프로그램의 전체적인 퍼포먼스를 저하시킨다.
- 다수의 사용자 요청을 처리하기 위해 서비스적인 측면으로 바라볼 때, 특히 대규모 프로젝트에서 중요하다. 다수의 사용자의 요청을 수용하고, 빠르게 처리하고 대응하기 위해 쓰레드 풀을 사용한다.
Thread Pool의 단점
- 과유불급.. 너무 많이 만들어 놓았다간 메모리 낭비만 발생 많은 병렬처리를 예상해서 1억개의 쓰레드를 만들어 놓았다고 생각해보자. 실제로 100개 정도의 요청과 병렬처리를 했다. 그렇다면 나머지 쓰레드들은 아무일도 하지 않고 메모리만 차지하는 최악의 경우가 발생 될 수 있다.
- 노는 쓰레드가 발생될 수 있다
예를 들어 A, B, C 3개의 쓰레드가 있는데, 병렬적으로 일을 처리하는 과정에서 A, B, C 작업 완료 소요시간이 다른 경우 쓰레드 유휴시간 즉 A 쓰레드는 아직 일이 많아서 허덕이고 있는데, B, C는 일을 다하고 A가 열심히 일하는 것을 보고 놀고만 있는 유휴시간이 발생된다.
자바에서는 이를 방지하기 위해 forkJoinPool을 지원한다. (-> 쓰레드당 걸리는 시간이 다를때 (한 쓰레드가 굉장히 오래 걸릴 때) 사용하면 좋음, 모두 동일할 때에는 쓸 때 없는 객체 생성을 없애기 위해 안쓰는게 좋음)
Thread Pool 생성 / 종료
- 쓰레드 풀 생성
ExecutorService 구현 객체는 Executors 클래스의 다음 두가지 메소드 중 하나를 이용해 간편하게 생성할 수 있다.
초기 쓰레드 수 : ExecutorService 객체가 생성될 때 기본적으로 생성되는 쓰레드 수
코어 쓰레드 수 : 쓰레드가 중가한 후 사용되지 않은 쓰레드를 쓰레드 풀에서 제거할 때 최소한으로 유지해야 할 수
최대 쓰레드 수 : 쓰레드 풀에서 관리하는 최대 쓰레드 수
newCachedThreadPool()
초기 쓰레드 수, 코어 쓰레드 수 0개, 최대 쓰레드 수는 integer 데이터 타입이 가질 수 있는 최대값(Integer.MAX_VALUE)
쓰레드 개수보다 작업 개수가 많으면 새로운 쓰레드를 생성하여 작업을 처리한다.
만약 60초 동안 아무일을 하지 않으면 쓰레드를 종료시키고 쓰레드 풀에서 제거한다.
newFixedThreadPool(int nThreads)
초기 쓰레드 개수는 0개, 코어 쓰레드 수와 최대 쓰레드 수는 매개변수 nThreads 값으로 지정, 이 쓰레드 풀은 쓰레드 개수보다 작업 개수가 많으면 마찬가지로 쓰레드를 생성하여 작업을 처리한다.
만약 놀고 있는 쓰레드가 있어도 제거하지 않고 그대로 둔다.
newCachedThreadPool(), newFixedThreadPool() 메서드를 사용하지 않고 직접 쓰레드 개수들을 설정하고 싶다면 직접 ThreadPoolExecutor 객체를 생성하면 된다.
- 쓰레드 풀 종료
쓰레드 풀에 속한 쓰레드는 기본적으로 데몬쓰레드(주 쓰레드를 서포트하기 위해 만들어진 쓰레드, 주 쓰레드 종료시 강제 종료)가 아니기 때문에 main 쓰레드가 종료되어도 작업을 처리하기 위해 계속 실행 상태로 남아있다. 즉 main() 메서드가 실행이 끝나도 어플리케이션 프로세스는 종료되지 않는다. 어플리케이션 프로세스를 종료하기 위해선 쓰레드 풀을 강제로 종료시켜 쓰레드를 해체시켜줘야한다.
ExecutorService 구현 객체에서는 기본적으로 3개 종료 메서드를 제공한다.
executorService.shutdown();
작업 큐에 남아있는 작업까지 모두 마무리 후 종료 (오버헤드를 줄이기 위해 일반적으로 많이 사용)
executorService.shutdownNow();
작업 큐 작업 잔량 상관없이 강제 종료
executorService.awaitTermination(long timeout, TimeUnit unit);
모든 작업 처리를 timeout 시간 안에 처리하면 true 리턴, 처리하지 못하면 작업 쓰레드들을 interrupt()시키고 false 리턴
Thread Pool에게 작업시키기
쓰레드 풀에게 작업을 시키기 전 작업을 생성시켜야 작업처리를 요청할 수 있다.
작업 생성은 Runnable 인터페이스와 Callable 인터페이스를 구현한 클래스로 작업요청할 코드를 삽입해 작업을 만들 수 있다.
둘의 차이점은 Runnable의 run() 메서드는 리턴값이 없고, Callable의 call() 메서드는 리턴값이 있다.
쓰레드 풀에게 작업 처리를 요청하기 위해선 execute(), submit() 2가지 메서드가 있다.
execute();
- 작업 처리 결과를 반환하지 않는다.
- 작업 처리 도중 에외가 발생하면 쓰레드가 종료되고 해당 쓰레드는 쓰레드 풀에서 제거된다.
- 다른 작업을 처리하기 위해 새로운 쓰레드를 생성한다.
submit();
- 작업 처리 결과를 반환한다.
- 작업 처리 도중 예외가 발생하더라도 쓰레드는 종료되지 않고 다음 작업을 위해 재사용
- 쓰레드의 생성 오버헤드를 방지하기 위해서라도 submit()을 가급적으로 사용한다.
package test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPoolExample {
public static void main(String[] args) {
// ExecutorService 인터페이스 구현객체 Executors 정적메서드를 통해 최대 스레드 개수가 2인 스레드 풀 생성
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i = 0; i < 10; i++){
Runnable runnable = new Runnable() {
@Override
public void run() {
//스레드에게 시킬 작업 내용
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
int poolSize = threadPoolExecutor.getPoolSize();//스레드 풀 사이즈 얻기
String threadName = Thread.currentThread().getName();//스레드 풀에 있는 해당 스레드 이름 얻기
System.out.println("[총 스레드 개수:" + poolSize + "] 작업 스레드 이름: "+ threadName);
//일부로 예외 발생 시킴
int value = Integer.parseInt("예외");
}
};
//스레드풀에게 작업 처리 요청
executorService.execute(runnable);
// executorService.submit(runnable);
//콘솔 출력 시간을 주기 위해 메인스레드 0.01초 sleep을 걸어둠.
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//스레드풀 종료
executorService.shutdown();
}
}