[Java]스레드풀(Thread Pool)

Wintering·2022년 5월 23일
0

이펙티브 자바

목록 보기
9/18

스레드풀

병렬 작업 처리가 많아지면 스레드 개수가 증가하고, 그에 따른 스레드 생성과 스케줄링으로 인해 CPU가 바빠져 메모리 사용량이 늘어난다. -> 애플리케이션 성능 저하
👉갑작스런 병렬 작업의 폭증으로 인한 스레드의 폭증을 막기위해 스레드풀(ThreadPool)사용

  • 스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리한다. 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리함. 때문에 작업 처리 요청이 폭증하여도 스레드의 전체 개수가 늘어나지는 않기 때문에 애플리케이션의 성능이 급격히 저하되지는 않는다.

  • ExecutorService 인터페이스와 , Executors 클래스를 사용해 구현


1. 스레드풀 생성과 종료

메소드명초기 스레드 수코어 스레드 수최대 스레드 수
newCachedThreadPool()00integer.MAX_VALUE
newFixedThreadPool(int nThreads)0nThreadsnThreads
  • 초기 스레드 수 : ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수
  • 코어 스레드 수 : 최소한으로 유지해야 할 스레드 수
  • 최대 스레드 수 : 스레드 풀에서 관리하는 최대 스레드 수

newChachedThreadPool()

초기 스레드수와 코어 스레드 수가 0이고, 스레드 개수가 작업 개수보다 많으면 새 스레드를 생성시켜 작업을 처리한다. 1개 이상의 스레드가 추가되었을 경우, 60초 동안 추가된 스레드가 아무 작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거한다.

🔻생성방법
ExecutorService executorService = Executors.newCachedThreadPool();

newFixedThreadPool(int nThreads)

이 스레드풀은 스레드가 작업을 처리하지 않고 놀고 있더라도 스레드 개수가 줄지 않는다.

🔻생성방법
ExecutorService executorService = Executors.newFixedThreadPool(
	Runtime.getRuntime().availableProcessors()
);

스레드풀 종료

스레드풀의 스레드는 기본적으로 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있다.
따라서 애플리케이션을 종료하려면 스레드풀을 종료시켜 스레드풀이 종료 상태가 되도록 처리해주어야 한다.

리턴타입메소드명(매개변수)설명
voidshutdonw()현재 처리중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드 풀을 종료시킨다.
List(Runnable)shutdownNow()현재 작업 처리 중인 스레드를 interrupt해서 작업 중지를 시도하고 스레드풀을 종료시킨다. 리턴 값은 작업 뉴에 있는 미처리 된 작업(Runnable)의 목록이다.
booleanawaitTermination(long tmieout,TimeUint unti)shutdown() 메소드 호출 이후, 모든 작업 처리를 timeout 시간 내에 완료하면 true를 리턴하고, 완료하지 못하면 작업 처리 중인 스레드를 interrupts하고 false를 리턴한다.

2. 작업 생성과 처리 요청

작업생성

하나의 작업은 Runnable 또는 Callable구현 클래스로 표현한다.
둘의 차이는 리턴값이 없느냐, 있느냐 이다.

<//Runnable
Runnable task - new Runnable(){
	@Override
    public void run(){
    	//작업내용
    }
}

//Callable
Callable<T> task = new Callable<T>(){
	@Override
    public T call() throws Exception{
    	//작업내용
        return T;
    }
}

작업처리요청

ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위

리턴 타입메소드명(매개변수)설명
voidexecute(Runnable command)Runnable을 작업 큐에 저장 / 작업 처리 결과를 받지 못함
Future<?>submit(Runnable task)Runnable 또는 Callable을 작업 큐에 저장 / 리턴 된 Future를 통해 작업 처리 결과를 얻을 수 잇음
submit(Runnable task, V result)
subit(Callable task)
  • execute()는 작업 처리 결과를 받지 못하고, submit()은 작업 처리 결과를 받을 수 있도록 Future를 리턴한다.
  • execute()는 작업 처리 도중 예외가 발생하면 스레드가 종료되고, 해당 스레드는 스레드 풀에서 제거 된다.
    submit()은 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용된다.
    👉가급적이면 스레드의 생성 오버헤더를 줄이기 위해서 submit()을 사용하는 게 바람직

블로킹 방식의 작업 완료 통보

  • submit() 메소드가 리턴한 Future객체는 작업 결과가 아니라 작업이 완료될 때 까지 기다렸다가 (=블로킹했다가 = 지연했다가) 최종 결과를 얻는데 사용된다.
  • Future = 지연완료객체(pending completion)
  • Future의 get() 메소드를 호출하면 스레드가 작업을 완료할 때 까지 블로킹 되었다가, 작업을 완료하면 처리 결과를 리턴한다.
  • 🚨주의 : 작업을 처리하는 스레드가 작업을 완료하기 전까지는 get() 메소드가 블로킹되므로 다른 작업을 실행할 수 없다. 때문에 get() 메소드를 호출하는 스레드는 새로운 스레드거나, 스레드 풀의 또 다른 스레드가 되어야 한다.

//새로운 스레드를 생성해서 호출
new Thread(new Runnable(){
	@Override
	public void run(){
		try{
			future.get();
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}).start();

//스레드풀의 스레드가 호출
executorService.submit(new Runnable(){
	@Override
	public void run(){
		try{
			future.get();
		}catch(Exception e){
			e.printStackTrace();
		}
	}
});
  • Future 객체는 작업 결과를 얻기 위한 get() 메소드 이외에도 다음과 같은 메소드를 제공한다.
리턴타입메소드명(매개변수)설명
booleancancle(boolean mayInterruptIfRunning)작업 처리가 진행중일 경우 취소시킴
booleanisCancelled()작업이 취소되었는지 여부
booleanisDone()작업 처리가 완료 되었는지 여부

리턴값이 없는 작업 완료 통보

  • 리턴값이 없는 작업일 경우는 Runnable 객체로 생성.
    리턴값이 없는 작업 처리 요청은 submit(Runnable task) 메소드르르 이용하면 됨
    -> 결과값이 없음에도 Future 객체를 리턴하는데, 이는 스레드가 작업처리를 정상적으로 완료했는지, 아니면 작업 처리도중에 예외가 발생했는지 확인하기 위해서임
 Future future = executorService.submit(task);
  • 작업 처리가 정상적으로 완료되면 get() 은 null을 리턴,
    작업 처리 도중 interrupt 되었따면, InterruptedExecption을 발생시키고,
    작업 처리 도중 예외가 발생하면, ExecutionException을 발생시킴. -> 예외처리코드 필요

try{
	future.get();
}catch(InterruptedExeption e){
}catch(ExecutionException e){}

public class NoResultExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
        );

        System.out.println("[작업처리요청]");
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for (int i = 1; i <= 10; i++) {
                    sum += i;
                }
                System.out.println("[처리결과] " + sum);
            }
        };
        Future future = executorService.submit(runnable);

        future.get();
        System.out.println("[작업처리완료]");
    }
}

리턴값이 있는 작업 완료 통보

  • 스레드풀의 스레드가 작업을 완료한 후에 애플리케이션 처리 결과를 얻어야 한다면 Callable로 생성하면 됨
    ExecutorService의 submit() 메소드를 호출하면, submit() 메소드는 작업 큐에 Callable 객체를 저장하고 즉시 Future를 리턴한다.

Future<T> future = executorService.submit(task)

public class ResultByCallableExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors());

        System.out.println("[작업처리요청]");
        Callable<Integer> task = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 10; i++) {
                    sum += 1;
                }
                return sum;
            }
        };
        Future<Integer> future = executorService.submit(task);
        int sum = future.get();
        System.out.println("[처리결과]" +sum);
        System.out.println("[작업처리완료]");
        executorService.shutdown();
    }
}

작업 완료 순으로 통보

  • 스레드풀에서 작업 처리가 완료된 것만 통보받는 방법
    CompletionService -> CompletionService는 처리 완료 된 작업을 가져오는 poll()take() 메소드 제공
executorService.submit(new Runnable){
	@Override
	public void run(){
		while(true){
			//완료된 작업이 잇을때까지 블로킹, 완료된 작업이 있으면 Future를 리턴
			Future<Integer> future = completionService.take();	
			//get()은 블로킹 되지 않고 바로 작업 결과를 리턴
			int value = future.get();
			System.out.println(value);
		}
	}
}
  • take() 메소드가 리턴하는 완료된 작업은 submit()으로 처리 요청한 작업의 순서가 X

콜백방식의 작업완료 통보

  • 콜백방식 : 애플리케이션이 스레드에 작업 처리를 요청한 후, 스레드가 작업을 완료하면 특정 메소드를 실행하는 기법
    콜백방식은 작업 처리를 요청한 후 결과를 기다릴 필요없이 다른 기능을 수행할 수 있다.
    작업 처리가 완료 되면 자동적으로 콜백 메소드가 실행되서 결과를 알 수 있기 때문!

  • ExecutorService는 콜백을 위한 별도의 기능을 제공하지 X
    Runnable 구현 클래스를 작성할때 콜백 기능을 구현 할 수 있다. 직접 정의하거나 java.nio.channels.CompletionHandler 이용 (비동기 통신의 콜백 객체를 만들때 사용)

<CompletionHandler<v,A> callback = new Completionhandler<V,A>(){
	@Override
	public void completed(V result, A attachment){}
	@Override
	public void fialed(V result, A attachement){}
}

0개의 댓글