ThreadPoolExecutor을 이용한 효율적인 멀티쓰레드 이용
by 키위먹고싶다쓰레드 풀을 사용하는 이유
- 병렬 작업 처리가 많아지면 쓰레드의 개수가 늘어나고, 쓰레드의 생성과 스케줄링으로 인해 CPU의 사용률이 저하되고, 메모리의 사용량이 늘어난다. 이렇게 되면 어플리케이션의 성능이 저하된다. 쓰레드를 한 번 생성할때마다 OS가 해당 쓰레드를 위한 스택을 확보하고, 작업이 끝나면 다시 이 메모리를 회수하는 작업은 처리시간과 메모리 오버헤드가 큰 작업이다. 이는 비용이 큰 작업이며, 수행에 영향을 끼칠 수 있다. 이러한 단점을 개선하기 위해 쓰레드 풀을 사용해서 쓰레드를 미리 만들어두고, 필요한 작업이 생길때 쓰레드들에게 작업을 분배하면 효율적으로 메모리를 사용할 수 있다.
쓰레드 풀 생성
자바에서는 쓰레드의 생성 및 수거에 대한 일을 효율적으로 지원하기 위해 ExecutorService 인터페이스와 Executor클래스를 제공하여 쓰레드 풀을 구현할 수 있다. 쓰레드 풀이 ExecutorService 객체이다.
초기 쓰레드 수 : ExecutorService객체가 생성될때 기본적으로 생성되는 쓰레드 수.
코어 쓰레드 수 : 쓰레드 수가 증가된 후 사용되지 않는 쓰레드를 쓰레드 풀에서 제거할때 최소한 유지해야 할 쓰레드 수
최대 쓰레드 수 : 쓰레드 풀에서 관리하는 생성될 수 있는 최대 쓰레드의 수.
ExecutorService executorService = Executors.newCachedThreadPool();
newCachedThreadPool()
- 초기 쓰레드 갯수, 코어 쓰레드 갯수 둘다 0임.
- 쓰레드 수보다 작업의 수가 많으면 새로운 쓰레드를 생성시켜서 작업을 처리함.
- 1개 이상의 쓰레드가 추가 되었을 경우 60초(60L, TimeUnit.SECONDS)동안 추가된 쓰레드가 아무 작업도 하지 않으면 쓰레드를 종료하고 풀에서 제거함.
ExecutorService executorService = Executors.newFixedThreadPool(6);
newFixedThreadPool(int nThread)
- 코어 쓰레드 수와 최대 쓰레드 수가 매개 변수로 준 nThread이다.
- 스레드가 작업을 처리하지 않고 놀고 있어도 쓰레드가 제거되지 않는다.
ExecutorService executorService = new ThreadPoolExecutor(
3,
10,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()
);
직접 ThreadPoolExecutor을 생성하는 방법도 있는데, 생성자에
코어 쓰레드 수, 최대 쓰레드 수, 놀고 있는 시간, 시간 단위, 작업 큐를 직접 지정할 수 있다.
이 쓰레드 풀은 초기 쓰레드 수가 0개, 코어 쓰레드 수가 3개, 최대 쓰레드 수가 10개인 쓰레드 풀을 생성한다.
그리고 코어 쓰레드 3개를 제외한 나머지 추가된 쓰레드 7개가 60초이상 작업하지 않을 경우,
해당 쓰레드를 제거해서 쓰레드의 수를 관리한다.
작업 생성
Runnable task = new Runnable() {
@Override
public void run() {
//쓰레드의 작업 내용
}
};
Callable<T> task = new Callable<T>() {
@Override
public T call() throws Exception {
//쓰레드의 작업 내용
return T;
}
};
Runnable또는 Callable 구현 클래스로 작업한다. 두개의 차이점은 리턴값이 있느냐 없느냐 이다.
Runnable의 run()메서드는 리턴값이 없고, Callable의 call()메서드는 리턴타입이 Callable을 implements한 구현체에서 지정한 T타입이다. 쓰레드 풀의 쓰레드는 작업큐에서 이 객체를 가져와 메서드를 실행한다.
작업 처리 요청
작업 처리 요청이란 ExecutorService의 작업 큐에 Runnable또는 Callable객체를 넣는 행위이다.
ThreadPool작업
쓰레드 풀에게 작업 처리 요청을 하기 위해서 execute(), submit() 2가지 메서드를 사용한다.
- execute()
- Runnable을 작업 큐에 저장함.
- 작업처리 결과를 반환하지 않는다.
- 작업 처리 중 예외가 발생하면 쓰레드가 종료되고 해당 쓰레드는 쓰레드 풀에서 제거된다.
- 그래서 다른 작업을 처리하기 위해 새로운 쓰레드를 생성한다.
for(int i=0; i<6; i++){
executorService.execute(
new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
int a = 10/0; //예외 발생
}
}
);
}
결과
- submit()
- Runnable, Callable을 작업 큐에 저장함.
- 작업 처리 결과를 반환한다.
- 작업 처리 중 예외가 발생해도 쓰레드가 종료되지 않고 다음 작업에 재사용된다.
- Future객체를 리턴해서 쓰레드 풀에서 처리한 결과를 알 수 있다.
- submit()사용을 권장한다.
ExecutorService executorService = newFixedThreadPool(2);
for(int i=0; i<6; i++){
executorService.submit(
new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
int a = 10/0; //예외 발생
}
}
);
}
쓰레드 풀 종료
쓰레드 풀에 속한 쓰레드는 데몬쓰레드가 아니기 때문에, main 쓰레드가 종료되더라도,
작업이 종료되지 않으면 계속 실행상태로 남아있다. (main()메서드 실행이 끝나도 다른 쓰레드가 끝나지 않아서 프로세스가 종료되지 않는것.) 그래서 프로세스를 끝내기 위해서는 쓰레드 풀을 강제로 종료시켜야 한다.
- shutdown()
- 작업큐에 있는 모든 작업을 끝내고 종료.
- shutdownNow()
- 작업 큐에 남아있는 작업에 상관없이 강제 종료.
- awaitTermination(long timeOut, TimeUnit unit)
- 모든 작업 처리를 timeOut시간 안에 처리하면 true리턴, 처리하지 못하면 작업 쓰레드들을 interrupt()시키고 false리턴
'java' 카테고리의 다른 글
stream에 대한 정리 (0) | 2021.12.22 |
---|---|
익명객체와 함수형인터페이스 '람다식' (0) | 2021.12.16 |
split 과 toCharArray (0) | 2021.12.15 |
Comparable, Comparator 에 관하여 (0) | 2021.12.12 |
쓰레드 (0) | 2021.12.10 |
블로그의 정보
kiwi
키위먹고싶다