1. 자바 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 스레드
2. Executors
- Thread를 만들고 관리하는 것을 고수준의 API Executors에게 위임합니다.
- Runnable만 개발자가 만들고 생성, 종료, 없애기 작업(일련의 작업)들은 Executors가 해줍니다.
- 인터페이스는 크게 Executor와 ExecutorService가 있으나 실질적으로 ExecutorService를 사용합니다.
- 개발자는 작업만 소스코드로 작성하면 됩니다.
Executors의 역할
- Thread 만들기: 애플리케이션이 사용할 Thread pool을 만들어 관리합니다.
- Thread 관리: Thread 생명 주기를 관리합니다.
- 작업 처리 및 실행: Thread로 실행할 작업을 제공할 수 있는 API를 제공합니다.
주요 인터페이스
[Executor]
public interface Executor {
void execute(Runnable command);
}
[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);
// (. . .) 생략
}
[ScheduledExecutorService]
- ExecutorService를 상속받은 인터페이스로 특정 시간 이후에 또는 주기적으로 작업을 실행할 수 있습니다.
package java.util.concurrent;
public interface ScheduledExecutorService extends ExecutorService {
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
}
3. 예시
SingleThread
- ExecutorService 인터페이스와 Executors 클래스의 static method를 이용해 ExecutorService를 구현하여 사용합니다.
- 사용 종료 후에는 종료 명령어를 이용해서 종료해야 합니다. 그렇지 않을 경우엔 다음 작업이 들어올 때까지 무한 대기를 합니다.
[주요 소스코드]
1. 구현체 생성 -> Executors.newSingleThreadExecutor()
: Executors 클래스의 Static Method를 활용하여 ExecutorService 구현체를 SingleThread 형태로 리턴해줍니다.
2. 작업 제출 -> submit()
: Thread를 활용할 작업을 제출합니다.(해당 스레드가 대기 중인 경우 제출한 작업을 처리합니다.)
3. 작업 종료 -> shutdown()
: 현재 진행 중인 작업을 마치고 Thread를 종료합니다. (꼭 종료해야 합니다. 그렇지 않을 경우 종료하지 않고 무한 대기합니다.)
즉시 종료 -> shutdownNow()
: 현재 진행 중인 작업을 마치지 않은 채로 종료할 수도 있습니다.(즉시 종료)
[예시]
public static void SingleThread() {
// 1. [Single Thread ExecutorService 구현체 생성]
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 2. [Runnable 작업 제출]
executorService.submit(() -> System.out.println("Thread Test: " + Thread.currentThread().getName()));
// 3. [graceful shutdown]
executorService.shutdown();
// [즉시 종료]
executorService.shutdownNow();
}
[실행 결과]
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() + ") ");
}
[실행 결과]
- 실행 결과를 보면 스레드 사용 시 실행 순서는 보장되지 않는 것을 확인할 수 있습니다. (아래의 소스코드가 먼저 실행되는 경우도 있습니다.)
ScheduledThread
- 특정 시간 이후에 또는 주기적(반복적)으로 작업을 실행할 수 있습니다.
- 크론이나 스케줄러처럼 사용할 수도 있습니다.
[예시 1] -> 10초 대기 후에 작업 처리(실행)
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
System.out.println("실행: " + LocalDateTime.now());
// 매개변수 -> (Runnable, 대기시간, 시간단위)
scheduledExecutorService.schedule(printRunnable("스레드 테스트"), 10, TimeUnit.SECONDS); // 3초 정도 있다가 실행하라
scheduledExecutorService.shutdown();
}
[실행 결과]
[예시 2] -> 1초 대기 후 10초에 1번씩 작업 처리(실행)
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
System.out.println("실행: " + LocalDateTime.now());
// 1초 기다렸다가 10초에 한 번씩 실행 (shutdown 지우기) (Runnable, 대기시간, 주기, 단위)
scheduledExecutorService.scheduleAtFixedRate(printRunnable("스레드 테스트"), 1, 10, TimeUnit.SECONDS);
}
private static Runnable printRunnable(String message) {
return () -> System.out.println(message + " (" + Thread.currentThread().getName() + ", " + LocalDateTime.now() + ") ");
}
[실행 결과]
'Backend > Java' 카테고리의 다른 글
[Java] Json 문자열 객체 형태로 바꾸기 (Json String To DTO) (0) | 2022.01.12 |
---|---|
[Java] 자바 8 Stream API 루핑 (peek, forEach) (0) | 2022.01.07 |
[Java] 자바 Thread 주요 기능(sleep, interrupt, join) (0) | 2022.01.05 |
[Java] 자바 Thread 생성하기 (0) | 2022.01.04 |
[Java] 자바 8 Stream API 필터링 (filter, distinct) (0) | 2022.01.01 |