[JAVA] ExecutorService를 이용한 멀티 쓰레드

Kevin·2024년 4월 10일
1

JAVA

목록 보기
9/16
post-thumbnail

서론

이번에 멀티 쓰레드를 통해서 특정 로직을 빠르게 실행 해야하는 순간이 있었다.

최근 Python에서도 멀티 쓰레드를 이용한 프로그램을 구현한 경험이 있었기에, JAVA로의 멀티 쓰레드 구현도 어렵지는 않았다.

Python에서도 마찬가지로 ExecutorService 라이브러리를 통해 편하게 멀티 쓰레드를 구현하였었는데, JAVA에서 ExecutorService 라이브러리는 어떤 역할을 할까?

ExecutorService 라이브러리는 병렬 작업시에 여러 개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA 라이브러리이며, Task만 지정해두면 알아서 ThreadPool을 이용해서 Task를 실행하고 관리한다.

이 때 Task는 Queue로 관리되며, ThreadPool에 있는 Thread 수 보다 Task가 많으면, 미 실행된 Task는 Queue에 저장되고, 실행을 마친 Thread로 할당되어 순차적으로 수행된다.

이제 ExecutorService를 사용했던 실제 코드에 대해서 이야기 해보자.


ExecutorService 실제 코드

            // 멀티 쓰레드 작업을 위한 Executor 생성
            ExecutorService executor = Executors.newFixedThreadPool(targets.size());

            for (HashMap<String, Object> target : targets) {
                executor.execute(() -> {
                 
	                // 각 원소에 대한 특정 로직 호출
                	modmTargetService.getModmInfoOID(target);
                });
            }

            // 모든 작업이 완료될 때까지 대기
            executor.shutdown();
            while (!executor.isTerminated()) {}

위가 ExecutorService를 사용한 실제 코드이다.

ExecutorService를 이용해 멀티 쓰레드를 구현할 때는 당연하게도 ExecutorService 객체를 먼저 만들어야 한다.

ExecutorService 객체를 생성하는 방법은 ThreadPoolExecutor 생성자를 통해 생성하는 방법과 Executors 클래스에서 제공하는 정적 팩토리 메서드를 통해 생성하는 방법이 있다.

나는 여기서 Executors 클래스의 정적 팩토리 메서드를 통해 ExecutorService 객체를 생성하였다.

그 후 생성한 ExecutorService에게 Task를 시킨다.

Task를 ThreadPool에게 할당 하기 위해서 execute()submit() 등의 방식을 통해서 Task를 할당한다.

마지막으로 ExecutorService를 shutdown()을 통해서 종료한다.

위 과정을 정리하면 아래와 같다.

  1. ExecutorService 객체 생성
  2. Task를 ThreadPool에게 할당
  3. ExecutorService 객체를 종료

이제 내가 구현한 코드에 대해서 설명을 해보겠다.

먼저 ExecutorService 객체를 Executors 객체가 제공하는 정적 팩토리 메서드를 통해 생성한다.

그 다음 execute()를 통해서 Task를 할당 시키는데, for문으로 사용함으로써 동기적 호출이 되는 거 아닌가라는 생각을 하게 된 분들을 위해 보충 설명을 해보겠다.

이 때 executor.execute()가 호출 되는대로 for문은 그 다음 target으로 넘어간다.

즉 for문에 의해서 순차적으로 실행되긴 하지만, 이는 동기적 실행이 아니라 2번째 for문이 1번째 for문보다 일찍 끝날 수 있는 비동기적 실행이다.

이게 가능한 이유는 excute() 메서드는 후술하겠지만, 인자로 받은 task를 작업 큐에 등록하는 역할이기에 메서드를 호출하는 task를 작업 큐에 등록만 하고, 종료한다.

그러면 Executor가 내부적으로 작업 큐에 등록된 작업들을 알아서 적절한 쓰레드를 사용해 처리한다.

newFixedThreadPool()은 인자 갯수만큼 고정된 쓰레드 풀을 생성한다.

executor.execute(Runnable task)은 Runnable 타입의 task를 작업 큐에 저장한다.

→ 이 때 task의 타입이 Runnable 이므로 작업의 결과를 받아오지는 못한다.

→ 작업의 결과를 반환받고 싶다면 submit()을 사용하자. submit()은 원하는 작업을 작업 큐에 등록하면, 그 즉시 Future 객체를 돌려받는다.

executor.shutdown()은 현재 처리하고 있는 작업과 작업 큐에 들어있는 모든 작업을 처리한 뒤에 스레드 풀을 종료시킨다는 뜻의 메서드이다.

그리고 스레드 풀을 종료하는 동안 해당 코드가 종료되면 안되기에, 아래의 while 루프를 통해서 대기한다.

while (!executor.isTerminated()) {}

profile
Hello, World! \n

0개의 댓글

관련 채용 정보