|
| 1 | +# 스레드 풀과 Executor 프레임워크 |
| 2 | + |
| 3 | +## 스레드를 직접 사용할 때의 문제점 |
| 4 | +1. 스레드 생성 시간으로 인한 성능 문제 |
| 5 | + - 메모리 할당 (스레드 별 스택 메모리) |
| 6 | + - OS 자원/스케줄러 활용 (시스템 콜 및 스케줄링에 따른 오버헤드) |
| 7 | +2. 스레드 관리 문제 |
| 8 | + - 시스템이 유지될 수 있는 최대 스레드의 수 까지만 제어해야 함 |
| 9 | +3. `Runnable` 인터페이스의 불편함 |
| 10 | + - 반환 값이 없고, 예외 처리가 불편함 |
| 11 | + |
| 12 | +<br> |
| 13 | + |
| 14 | +## Executor 프레임워크 |
| 15 | +자바의 Executor 프레임워크는 멀티스레딩 및 병렬 처리를 쉽게 사용할 수 있도록 돕는 기능의 모음 |
| 16 | + |
| 17 | +`ExecutorService` 인터페이스의 기본 구현체는 `ThreadPoolExecutor` |
| 18 | + |
| 19 | +```java |
| 20 | +class ExecutorTest { |
| 21 | + |
| 22 | + private static final ExecutorService executor = Executors.newFixedThreadPool(10); |
| 23 | + |
| 24 | + void doSomething() { |
| 25 | + executor.submit(() -> { |
| 26 | + // task |
| 27 | + }); |
| 28 | + } |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +`ExecutorService`는 `execute()` 메서드와 `submit()` 메서드 모두 호출이 가능 |
| 33 | +- `execute()` 메서드는 `Executor` 인터페이스의 구현 |
| 34 | +- `submit()` 메서드는 `ExecutorService` 인터페이스의 구현 |
| 35 | + |
| 36 | +차이점은 `execute()` 메서드는 `Runnable` 인터페이스를 구현한 객체만 인자로 받아 스레드 풀에 작업을 넣어줌 |
| 37 | + |
| 38 | +`submit()` 메서드는 `Runnable` 또는 `Callable` 인터페이스를 구현한 객체도 인자로 받아 스레드 풀에 작업을 넣어줌 |
| 39 | + |
| 40 | +또한 `submit()` 메서드는 `Future` 객체를 반환해줌 (`execute()` 메서드는 리턴 값을 반환하지 않음) |
| 41 | +- `Future` 객체는 작업의 상태를 확인하거나 작업의 결과를 가져올 수 있음 |
| 42 | + |
| 43 | +<br> |
| 44 | + |
| 45 | +### Runnable vs Callable |
| 46 | +`Runnable` 인터페이스는 `void` 타입의 `run()` 메서드를 가지고 있음 |
| 47 | +```java |
| 48 | +package java.lang; |
| 49 | + |
| 50 | +@FunctionalInterface |
| 51 | +public interface Runnable { |
| 52 | + void run(); |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +`Callable` 인터페이스는 제네릭 타입 설정을 통해 리턴 값을 반환하는 `call()` 메서드를 가지고 있음 |
| 57 | + |
| 58 | +```java |
| 59 | +package java.util.concurrent; |
| 60 | + |
| 61 | +@FunctionalInterface |
| 62 | +public interface Callable<V> { |
| 63 | + V call() throws Exception; |
| 64 | +} |
| 65 | + |
| 66 | +``` |
| 67 | + |
| 68 | +<br> |
| 69 | + |
| 70 | +### Future |
| 71 | +`Future` 는 작업의 미래 결과를 받을 수 있는 객체 |
| 72 | +- 비동기적인 작업의 결과를 나타내는 인터페이스 |
| 73 | +- `Future` 객체를 통해 작입이 진해될 때 요청 스레드는 대기하지 않고, 다른 작업을 수행할 수 있음 |
| 74 | + |
| 75 | +```java |
| 76 | +class ExecutorTest { |
| 77 | + |
| 78 | + private static final ExecutorService executor = Executors.newFixedThreadPool(10); |
| 79 | + |
| 80 | + void doSomething() { |
| 81 | + var future = executor.submit(() -> { |
| 82 | + // task |
| 83 | + }); |
| 84 | + |
| 85 | + val result = future.get(); |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +`future.get()` 을 호출했을 때 |
| 91 | +- **Future가 완료 상태**: `Future` 가 완료 상태면 `Future` 에 결과도 포함되어 있음 |
| 92 | + - 이 경우 요청 스레드는 대기하지 않고, 값을 즉시 반환받을 수 있음 |
| 93 | +- **Future가 완료 상태가 아님**: `task` 가 아직 수행되지 않았거나 또는 수행 중 |
| 94 | + - 이 때는 어쩔 수 없이 요청 스레드가 결과를 받기 위해 대기해야 함 |
| 95 | + - 요청 스레드가 마치 락을 얻을 때처럼 결과를 얻기 위해 대기 |
| 96 | + |
| 97 | +<br> |
| 98 | + |
| 99 | +### 종료 메서드 |
| 100 | +`void shutdown()` |
| 101 | +- 새로운 작업을 받지 않고, 이미 제출된 작업을 모두 완료한 후에 종료 |
| 102 | +- 논 블로킹 동작 (이 메서드를 호출한 스레드는 대기하지 않고 즉시 다음 코드를 호출) |
| 103 | + |
| 104 | + |
| 105 | +`List<Runnable> shutdownNow()` |
| 106 | +- 실행 중인 작업을 중단하고, 대기 중인 작업을 반환하며 즉시 종료 |
| 107 | +- 실행 중인 작업을 중단하기 위해 인터럽트를 발생 |
| 108 | +- 논 블로킹 동작 |
| 109 | + |
| 110 | + |
| 111 | +`close()` |
| 112 | +- 자바 19부터 지원하는 서비즈 종료 메서드 |
| 113 | +- `shutdown()` 을 호출하고, 하루(1일)를 기다려도 작업이 완료되지 않으면 `shutdownNow()` 를 호출 |
| 114 | +- 호출한 스레드에 인터럽트가 발생해도 `shutdownNow()` 를 호출한다. |
| 115 | + |
| 116 | +```java |
| 117 | +default void close() { |
| 118 | + boolean terminated = isTerminated(); |
| 119 | + if (!terminated) { |
| 120 | + shutdown(); |
| 121 | + boolean interrupted = false; |
| 122 | + while (!terminated) { |
| 123 | + try { |
| 124 | + terminated = awaitTermination(1L, TimeUnit.DAYS); |
| 125 | + } catch (InterruptedException e) { |
| 126 | + if (!interrupted) { |
| 127 | + shutdownNow(); |
| 128 | + interrupted = true; |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + if (interrupted) { |
| 133 | + Thread.currentThread().interrupt(); |
| 134 | + } |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +<br> |
| 140 | + |
| 141 | +## Executor 스레드 풀 관리 |
| 142 | +### 속성 |
| 143 | +```java |
| 144 | +public ThreadPoolExecutor( |
| 145 | + int corePoolSize, |
| 146 | + int maximumPoolSize, |
| 147 | + long keepAliveTime, |
| 148 | + TimeUnit unit, |
| 149 | + BlockingQueue<Runnable> workQueue |
| 150 | +) { |
| 151 | + this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +`ExecutorService` 의 기본 구현체인 `ThreadPoolExecutor` 의 생성자는 아래 속성을 사용 |
| 156 | +- `corePoolSize` : 스레드 풀에서 관리되는 기본 스레드의 수 |
| 157 | +- `maximumPoolSize` : 스레드 풀에서 관리되는 최대 스레드 수 |
| 158 | +- `keepAliveTime` , `TimeUnit unit` : 기본 스레드 수를 초과해서 만들어진 초과 스레드가 생존할 수 있는 대기 시간, 이 시간 동안 처리할 작업이 없다면 초과 스레드는 제거 |
| 159 | +- `BlockingQueue workQueue` : 작업을 보관할 블로킹 큐 |
| 160 | + |
| 161 | +<br> |
| 162 | + |
| 163 | +### Pool 생성 전략 |
| 164 | +- 고정 스레드 풀 전략 |
| 165 | + - **newFixedThreadPool(nThreads)** |
| 166 | + - 트래픽이 일정하고, 시스템 안전성이 가장 중요 |
| 167 | +- 캐시 스레드 풀 전략 |
| 168 | + - **newCachedThreadPool()** |
| 169 | + - 일반적인 성장하는 서비스 |
| 170 | +- 사용자 정의 풀 전략 |
| 171 | + - **ThreadPoolExecutor(...)** |
| 172 | + - 다양한 상황에 대응 |
| 173 | + |
| 174 | +대부분의 서비스는 트래픽이 어느정도 예측 가능하므로 일반적인 상황이라면 고정 스레드 풀 전략이나, 캐시 스레드 풀 전략을 사용하면 충분 |
0 commit comments