자바 Thread 관리의 어려움
- 자바로 스레드를 생성할 경우에는 아주 기본적으로는 아래와 같이 Thread 클래스와 Runnable 함수형 인터페이스를 구현해 Thread를 생성합니다.
- 간단한 소스 같은 경우에는 쉽게 관리할 수 있지만 복잡해지는 경우에는 스레드를 사용자가 직접 관리하는 것은 매우 어렵습니다.
- ex) 인터럽트 관리
- 이러한 관리의 어려운 문제를 해결하기 위해 스레드를 만들고 관리하는 작업을 위임을 하기 위해 Executors가 등장하게 됩니다.
// 람다로 스레드 만들기
Thread thread = new Thread(() -> {
System.out.println("Thread Test " + Thread.currentThread().getName());
});
thread.start();
System.out.println("Main Test " + Thread.currentThread().getName()); // main 스레드
Executors
- Thread를 만들고 관리하는 것을 고수준의 API Executors에게 위임합니다.
- Runnable만 개발자가 만들고 생성, 종료, 없애기 작업(일련의 작업)들은 Executors가 해줍니다.
- 인터페이스는 크게 Executor와 ExecutorService가 있으나 실질적으로 ExecutorService를 사용합니다.
- 개발자는 작업만 소스코드로 작성하면 됩니다.
- java.util.concurrent.Executors와 java.util.concurrent.ExecutorService를 이용하면 간단히 쓰레드풀을 생성하여 병렬처리를 할 수 있습니다.
ExecutorService 생성
Executors는 ExecutorService 객체를 생성하며, 다음 메소드를 제공하여 쓰레드 풀을 개수 및 종류를 정할 수 있습니다.
- new FixedThreadPool(int) : 인자 개수만큼 고정된 쓰레드풀을 만듭니다.
- new CachedThreadPool(): 필요할 때, 필요한 만큼 쓰레드풀을 생성합니다. 이미 생성된 쓰레드를 재활용할 수 있기 때문에 성능상의 이점이 있을 수 있습니다.
- new ScheduledThreadPool(int): 일정 시간 뒤에 실행되는 작업이나, 주기적으로 수행되는 작업이 있다면 ScheduledThreadPool을 고려해볼 수 있습니다.
- new SingleThreadExecutor(): 쓰레드 1개인 ExecutorService를 리턴합니다. 싱글 쓰레드에서 동작해야 하는 작업을 처리할 때 사용합니다.
다음은 4개의 고정된 쓰레드풀을 갖고 있는 ExecutorService를 생성하는 코드입니다.
ExecutorService executor = Executors.newFixedThreadPool(4);
ExecutorService
- Executor 상속받은 인터페이스로, Callable도 실행할 수 있으며, Executor를 종료시키거나, 여러 Callable을 동시에 실행하는 등의 기능을 제공한다.
- Runnable은 리턴 값이 없습니다. 그러나 Callable은 특정 타입의 객체를 리턴할 수 있습니다. (리턴 유무의 차이)
- public interface Runnable { public abstract void run(); }
- public interface Callable<V> { V call() throws Exception; }
- Thread 사용 시 실질적으로 사용하는 인터페이스입니다.
- 주로 Executors 클래스의 Static Method를 활용해 구현하여 사용합니다.
package java.util.concurrent;
import java.util.Collection;
import java.util.List;
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
// (. . .) 생략
}
3. 예시
SingleThread
- ExecutorService 인터페이스와 Executors 클래스의 static method를 이용해 ExecutorService를 구현하여 사용합니다.
- 사용 종료 후에는 종료 명령어를 이용해서 종료해야 합니다. 그렇지 않을 경우엔 다음 작업이 들어올 때까지 무한 대기를 합니다.
[주요 소스코드]
1. 구현체 생성 -> Executors.newSingleThreadExecutor()
: Executors 클래스의 Static Method를 활용하여 ExecutorService 구현체를 SingleThread 형태로 리턴해줍니다.
2. 작업 제출 -> submit()
: Thread를 활용할 작업을 제출합니다.(해당 스레드가 대기 중인 경우 제출한 작업을 처리합니다.)
3. 작업 종료 -> shutdown()
: 현재 진행 중인 작업을 마치고 Thread를 종료합니다. (꼭 종료해야 합니다. 그렇지 않을 경우 종료하지 않고 무한 대기합니다.)
즉시 종료 -> shutdownNow()
: 현재 진행 중인 작업을 마치지 않은 채로 종료할 수도 있습니다.(즉시 종료)
ExecutorService에 작업(Job) 추가
Executors로 ExecutorService를 생성하였다면, ExecutorService는 작업을 처리할 수 있습니다. ExecutorService.submit() 메소드로 작업을 추가하면 됩니다.
아래 코드에서 newFixedThreadPool(4)는 Thread를 4개 생성하겠다는 의미입니다. 그리고 submit(() -> { })은 멀티쓰레드로 처리할 작업을 예약합니다. 인자로 람다식을 전달할 수 있습니다.
아래 코드에서 4개의 작업을 예약했고, 예약과 동시에 먼저 생성된 4개의 쓰레드는 작업들을 처리합니다.
package test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceTest {
public static void main(String args[]) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Job1 " + threadName);
});
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Job2 " + threadName);
});
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Job3 " + threadName);
});
executor.submit(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Job4 " + threadName);
});
// 더이상 ExecutorService에 Task를 추가할 수 없습니다.
// 작업이 모두 완료되면 쓰레드풀을 종료시킵니다.
executor.shutdown();
// shutdown() 호출 전에 등록된 Task 중에 아직 완료되지 않은 Task가 있을 수 있습니다.
// Timeout을 20초 설정하고 완료되기를 기다립니다.
// 20초 전에 완료되면 true를 리턴하며, 20초가 지나도 완료되지 않으면 false를 리턴합니다.
if (executor.awaitTermination(20, TimeUnit.SECONDS)) {
System.out.println(LocalTime.now() + " All jobs are terminated");
} else {
System.out.println(LocalTime.now() + " some jobs are not terminated");
// 모든 Task를 강제 종료합니다.
executor.shutdownNow();
}
System.out.println("end");
}
}
로그를 보면 두개의 쓰레드가 4개의 작업을 모두 처리하였습니다. 대부분 예약한 순서대로 작업이 처리가 되지만, 간혹 쓰레드가 지연이 되어 순서가 뒤바뀌는 일이 발생할 수 있습니다.
shutdown()은 더 이상 쓰레드풀에 작업을 추가하지 못하도록 합니다. 그리고 처리 중인 Task가 모두 완료되면 쓰레드풀을 종료시킵니다.
awaitTermination()은 이미 수행 중인 Task가 지정된 시간동안 끝나기를 기다립니다. 지정된 시간 내에 끝나지 않으면 false를 리턴하며, 이 때 shutdownNow()를 호출하면 실행 중인 Task를 모두 강제로 종료시킬 수 있습니다.
MultiThread
- 위와 같이 마찬가지로 Executors 클래스의 static method를 이용해 ExecutorService를 구현하여 사용합니다.
[주요 소스코드]
1. 구현체 생성 -> Executors.newFixedThreadPool(Thread 개수)
: Executors 클래스의 Static Method를 활용하여 ExecutorService 구현체를 MultiThread 형태로 리턴해줍니다.
2. 작업 제출 -> submit()
: Thread를 활용할 작업을 제출합니다.(해당 스레드가 대기중인 경우 제출한 작업을 처리합니다.)
3. 작업 종료 -> shutdown()
: 현재 진행 중인 작업을 마치고 Thread를 종료합니다. (꼭 종료해야 합니다. 그렇지 않을 경우 종료하지 않고 무한 대기합니다.)
★ 스레드는 2개인데 작업은 4개라면 어떻게 되나요?
: 그럴 경우 스레드에서 작업을 바로 처리를 못하게 됩니다.
처리하지 못한 작업은 Blocking Queue에 작업을 쌓아서 대기해 둔 상태 두고 앞의 작업이 끝난 후에 작업을 처리합니다.
[예시]
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2); //Thread 2
executorService.submit(addRunnable(1,2));
executorService.submit(addRunnable(1,3));
executorService.submit(addRunnable(1,4));
executorService.submit(addRunnable(1,5));
executorService.shutdown();
}
private static Runnable addRunnable(int num1, int num2) {
return () -> System.out.println("result: " + (num1 + num2) + " (" + Thread.currentThread().getName() + ") ");
}
[실행 결과]
- 실행 결과를 보면 스레드 사용 시 실행 순서는 보장되지 않는 것을 확인할 수 있습니다. (아래의 소스코드가 먼저 실행되는 경우도 있습니다.)
'Language > Java' 카테고리의 다른 글
[Java] 동시성 이슈 해결하기 (0) | 2023.08.01 |
---|---|
[Java] CountDownLatch (0) | 2023.08.01 |
Enum(EnumMap, EnumSet) (0) | 2023.07.06 |