[Java Library] Executor Framework


Executors는 JDK에서 제공하는 framework로서 Java application에서 실행되는 task를 간단하게 비동기로 처리할 수 있게 해주는 thread-pool과 API를 제공합니다. Java application 상에서 thread를 한 두개를 만들어 돌리는 것은 그렇게 어렵지 않습니다. 하지만 그 숫자가 20, 30 혹은 그보다 많아질 경우에는 이 많은 thread를 어떻게 관리할 것인지 문제가 되기 시작합니다. 이 문제를 Executors framework을 통해 간단히 처리할 수 있습니다.


Executors framework가 하는 일은 크게 3가지 입니다.


1. Thread 생성 : thread를 생성하거나 thread pool을 만드는 method를 제공합니다.


2. Thread 관리 : thread의 생명주기를 관리합니다. thread pool이 활성화되어 있는지 죽은 상태인지에 대해 고려하지 않아도 되게끔 thread를 관리합니다.


3. Task 제출 및 실행 : Runnable 혹은 Callable method를 제출하고 그것을 원하는 때에 실행할 수 있게 해줍니다.



| ExecutorService instance 만들기


ExecutorService의 instance를 만드는 가장 쉬운 방법은 Executors class에서 제공하는 factory method 중 하나를 사용하는 겁니다. 예를 들어 10개의 thread가 있는 thread-pool을 만들고 싶다면 다음 코드와 같이 newFixedThreadPool을 사용하면 됩니다. 

ExecutorService executor = Executors.newFixedThreadPool(10);

위 예시말고 다른 요구사항에 맞는 executor instance를 만들고 싶다면 오라클 공식 문서를 참조하셔서 구현하시면 됩니다.


이외에도 java.util.concurrent 패키지를 이용하여 직접 new 키워드를 써서 구현하는 방법이 있습니다. 



ExecutorService에게 task 할당하기 


ExecutorService는 Runnable과 Callable task들을 실행할 수 있습니다. Runnable과 Callable은 모두 Functional Interface이고, 이 Interface에 ExecutorService에서 실행할 함수를 할당하는 방식이죠. 아래 코드는 그에 관한 예시입니다.        

Task들은 ExecutorService의 method들을 이용하여 ExecutorService Instance에 할당할 수 있거나 실행될 수 있습니다. 


다음은 executorService에서 제공하는 대표적인 method들입니다.


execute()는 Runnable task를 ExecutorService에 할당하여 미래 어떤 특정 시점에 실행되도록 합니다. 이 때, 어떠한 성공적으로 실행됬는지에 대한 체크는 하지 않습니다.

executorService.execute(runnableTask);


submit()는 Callable 혹은 Runnable task를 ExecutorService에게 제출하고 Future라는 Functional Interface의 Instance를 반환합니다. 이 Future Interface는 제출한 task의 상태를 확인할 때 사용될 method를 호출할 때 사용됩니다.


invokeAny()는 ExecutorService에 task들을 할당하고 각각의 task들을 실행시킵니다.


invokeAll()는 invokeAny()와 같으나 반환값으로 각각의 task들의 Future Interface를 List 형태로 반환합니다.

ExecutorService 예제


다음은 Executor framework를 사용한 예제입니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorMain {

	public static void main(String[] args) {
		ExecutorService executorService = Executors.newSingleThreadExecutor();
		
		Runnable runnable = () -> {
			System.out.println("inside : " + Thread.currentThread().getName());
		};
		
		executorService.submit(runnable);
	}
}

위 예제에서는 단일 thread를 생성하고 관리하기 위해 newSingleThreadExecutor를 사용하였습니다. 만일 task가 실행되기 위해 제출되면, 다른 task를 실행하느라 바쁜 상태에 있는 thread를 제외하고 thread pool에 있는 유휴 thread가 할당되기를 기다립니다. 이 때, 제출된 task는 queue에 대기하는 상태로 됩니다.


프로그램을 실행하면 프로그램이 종료되지 않는다는 것을 알 수 있습니다. 왜냐하면 명시적으로 application을 종료하지 않는 이상 ExecutorService는 다른 새로운 task가 들어올 때까지 대기하고 있기 때문입니다.


ExecutorService 종료하기


ExecutorService는 executor service를 종료하기 위해 두 가지 method를 제공합니다.


1. shutdown() : executor service에서 이 method가 실행될 때, 새로운 task가 할당되는 것을 막고 이미 전에 제출된 task가 실행되는 것을 기다린 후 executor instance를 종료합니다.


2. shutdownNow() : exeutor service를 곧바로 종료합니다.


아래는 executor service를 종료하는 코드를 추가한 예제입니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorMain {
	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(2);
		Runnable task1 = () -> {
			System.out.println("Executing Task1 inside : " + Thread.currentThread().getName());
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (Exception e) {
				e.printStackTrace();
			}
		};
		executorService.submit(task1);

		executorService.shutdown();
	}
}


다중 threads 및 tasks를 ExecutorService로 구현한 예제


다음은 ExecutorService를 통해 다중 threads를 생성하고 관리하는 예제입니다.

// output Executing Task2 inside : pool-1-thread-2 Executing Task1 inside : pool-1-thread-1 Executing Task3 inside : pool-1-thread-1

위 예제에서는 newFixedThreadPool를 사용하여 2개의 고정된 thread들을 관리하는 thread pool을 생성하였습니다. 고정된 thread pool에서는 executor service가 생성한 pool이 언제나 특정한 thread들이 실행된다는 것을 보장하죠. thread pool에서는 pool안의 thread가 죽더라도 그 자리를 새로운 thread가 즉시 대체하게 되어있습니다.


새로운 task가 제출되면 executor service는 현재 할당이 가능한 thread를 pool에서 뽑습니다. 그리고 그 task를 뽑은 thread에 할당하죠. 만약 thread들이 이전에 할당된 task들을 처리해야해서 할당가능한 thread가 존재하지 않으면, 그 이후에 제출된 task들은 queue에서 대기하게 됩니다.



Executor Service와 Thread Pool 구조

                                             

Executor Service는 Thread Pool과 Blocking Queue로 구성되어 있습니다. 제출된 task들은 blocking queue에 들어가게 되고 위에서 언급한 메커니즘에 의해서 유휴 thread에 할당됩니다. ( thread 남음 => 할당 받음, thread가 안 남음 => 대기함 )


thread를 생성하는 것은 비용이 큰 작업이므로 이 작업을 최소화 하기 위해 Executor service에서는 미리 thread pool안에 thread를 생성해 놓고 관리합니다. 한 번 생성해 놓고 계속 재사용하는 것이죠.



ScheduledExecutorService 예제


다음은 executor service에서 scheduling을 통해 thread를 실행하는 예제입니다. newScheduledThreadPool method를 통해서 1개의 thread를 관리하는 pool을 만들고 5초 뒤 task를 실행합니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ExecutorMain {

	public static void main(String[] args) {
		ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
		Runnable task = () -> {
			System.out.println("Executing Task At " + System.nanoTime());
		};
		
		System.out.println("Submitting task at " + System.nanoTime() + " to be executed after 5 seconds.");
		scheduledExecutorService.schedule(task, 5, TimeUnit.SECONDS);
		
		scheduledExecutorService.shutdown();
	}
}
//output 
Submitting task at 53978948987383 to be executed after 5 seconds.
Executing Task At 53983950911138


다음은 주기적으로 task를 실행하는 예제입니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ExecutorMain {

	public static void main(String[] args) {
		ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
		
		Runnable task = () -> {
			System.out.println("Executing at " + System.nanoTime());
		};
		
		System.out.println("scheduling task to be executed every 2 seconds with an initial delay of 0 seconds");
		scheduledExecutorService.scheduleAtFixedRate(task, 0, 2, TimeUnit.SECONDS);
	}
}

scheduling task to be executed every 2 seconds with an initial delay of 0 seconds
Executing at 54254984054269
Executing at 54256983981223
Executing at 54258984504146
Executing at 54260983313575
Executing at 54262986380456
Executing at 54264984489469
Executing at 54266983327570


마치며


Executor Framework는 Java application에서 Thread를 생성하고 관리하기 쉽게 다양한 method와 interface를 제공합니다. 당연한 것이지만 executors와 thread pool가 제공하는 모든 기능을 다 다룬 것은 아닙니다. 그 외의 Executor에 관한 내용은 오라클 문서를 참조하셔서 project 상황에 맞게 적용하시면 될 것 같습니다.


출처 : https://www.callicoder.com/java-executor-service-and-thread-pool-tutorial/

        https://www.baeldung.com/java-executor-service-tutorial

    
 


이 글을 공유하기

댓글(0)

Designed by JB FACTORY