지금까지 쓰레드를 배우면서 최대 세 개의 쓰레드를 생성했습니다. 요즘 CPU 성능에서 세 개 쯤이야(게다가 아주 간단한 작업들만 했으므로) 거뜬하지만 쓰레드가 늘어나거나 병렬 처리를 하는 작업이 복잡해지면 CPU와 메모리 사용량이 늘어나고 이는 곧 소프트웨어의 성능 저하로 이어지게 됩니다. 따라서 쓰레드가 너무 많이 늘어나는 것을 막고자 한다면 쓰레드풀
이라는 기술을 이용합니다.
쓰레드풀
은 작업 처리에 사용될 쓰레드의 수를 미리 제한해두고 작업 큐의 작업을 쓰레드가 하나씩 맡아서 처리하는 방식을 의미합니다. 작업 큐로부터 받은 작업이 끝나면 큐에서 다른 작업을 꺼내와서 계속해서 작업하게됩니다.
자바에서는 쓰레드풀을 지원하기 위해 java.util.concurrent
패키지에서 ExecutorService 인터페이스
, Executors 클래스
를 제공하고 있습니다.
Executors 클래스
의 정적 메소드 newCachedThreadPool(), newFixedThreadPool()
을 통해 ExecutorService
구현 객체를 생성해서 쓰레드풀을 생성할 수 있습니다.
메소드 | 기본 생성 쓰레드 수 | 코어 수 | 최대 쓰레드 수 |
---|---|---|---|
newCachedThreadPool() | 0 | 0 | Interger.MAX_VALUE |
newFixedThreadPool(int n) | 0 | 증가한 만큼 | n |
코어 수는 쓰레드가 증가된 후 사용하지 않는 쓰레드 삭제 과정에서 최소한으로 남는 쓰레드의 수를 의미합니다.
정리하면 newCachedThreadPool()
는 초기 쓰레드 0개에서 시작하여 최대 Integer.MAX_VALUE(= 2147483647)
까지 생성할 수 있고, 사용하지 않는 쓰레드는 모두 제거합니다.
60초간 쓰레드에서 작업이 없으면 쓰레드를 삭제합니다.
newFixedThreadPool(int n)
는 초기 쓰레드 0개에서 시작하여 최대 n개 까지 생성할 수 있고, 사용하지 않는 쓰레드는 제거하지 않습니다.
메소드를 이용하지 않고 ThreadPoolExecutor
를 이용해서 직접 생성할 수도 있습니다.
ExecutorService 쓰레드풀이름 = new ThreadPoolExecutor(
코어 수,
최대 쓰레드 수,
유휴 시간(작업이 없는 시간),
유휴 시간 단위,
작업 큐
);
쓰레드풀
의 쓰레드들은 자동적으로 종료되지 않습니다. 따라서 쓰레드풀의 쓰레드를 종료하려면 ExecutorService
의 메소드를 사용해서 종료해야합니다.
메소드 | 설명 |
---|---|
void shutdown() | 현재 작업과 작업 큐에 대기하는 모든 작업을 처리한 후 쓰레드풀 종료 |
List<Runnable> shutdownNow() | 현재 작업은 inturrpt() 로 종료시키고 쓰레드풀을 종료 작업 큐의 나머지 작업들이 List로 반환됩니다. |
작업은 Runnable, Callable
구현 객체로 생성합니다. 두 구현 객체간 차이점은 Runnable
은 작업 후 리턴값이 없다는 것이고 Callable
은 작업 후 리턴값이 있다는 것입니다.
new Runnable {
@Override
public void run() {
//작업 내용
}
}
new Callable {
@Override
public T call() throws Exception {
//작업 내용
return T;
}
}
이렇게 생성한 작업은 처리 요청을 해야하는데, 작업 처리 요청은 작업 큐에 구현 객체를 넣는 것을 말합니다. 작업 처리 요청을 위해 ExecutorService 인터페이스
는 다음 두 가지 메소드를 제공하고 있습니다.
메소드 | 설명 |
---|---|
void execute(Runnable r) | Runnable 구현 객체를 작업 큐에 인큐 |
Future<T> submit(Callable<T> c) | Callable 구현 객체를 작업 큐에 인큐 작업 처리 결과를 리턴함 |
Future는 비동기 작업의 결과를 표현하는 인터페이스 타입입니다.
작업 처리 요청을 통해 구현 객체가 작업 큐에 들어가면 ExecutorService
는 처리할 쓰레드가 있는지 확인합니다. 생성이 가능하다면 새로 쓰레드를 생성합니다. 그리고 쓰레드는 작업 큐에서 구현 객체를 가져와서 run()/call()을 실행해서 작업을 처리합니다.
다음은 Runnable
구현 객체(익명)을 이용해서 쓰레드풀에서 작업을 처리하는 예제 코드입니다.
작업 자체는 0 ~ 10 루프를 돌면서 숫자를 하나씩 출력하는 것인데, 쓰레드풀을 5개 만들어서 작업을 하기 때문에 실행해보면 쓰레드풀에서 작업을 처리한 순서대로 출력이 되는 것을 볼 수 있습니다.
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i <= 10; i++) {
final int n = i;
executorService.execute(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "의 작업" +" n: " + n);
}
});
}
executorService.shutdown(); //쓰레드 종료
}
}
쓰레드 풀에서 동작한 다섯개의 쓰레드가 10개의 출력을 사이좋게 나눠서 처리하는 것을 확인할 수 있습니다.