# Thread Pool

- 쓰레드 풀(thread pool)은 병렬 컴퓨팅에 사용되는 소프트웨어 디자인 패턴이다.
- 이 패턴은 프로그램이 프로그램의 수명 주기 동안 생성하고 소멸하는 쓰레드의 수를 미리 결정함으로써 일반적인 비용을 최소화하는 데 중점을 둔다
- 쓰레드 풀은복잡한 작업을 여러 쓰레드로 분할하여 동시에 실행할 수 있으므로, 멀티코어 프로세서의 이점을 최대한 활용할 수 있다.
- 쓰레드 풀은 고성능 컴퓨팅, 웹 서버, 데이터베이스, 그래픽 렌더링 등 많은 컴퓨팅 작업에서 활용된다.
- 쓰레드 풀은 프로그램의 초기화 단계에서 생성되며, 이는 프로그램이 요구에 따라 쓰레드를 생성하거나 종료하는 데 드는 비용을 절감하고, 쓰레드의 최대 수를 미리 결정함으로써 시스템 자원의 과다한 사용을 방지한다.
- 쓰레드 풀에서는 일반적으로 다음과 같은 작업을 수행한다
1. **쓰레드 생성**: 쓰레드 풀은 프로그램 시작 시에 일정 수의 쓰레드를 생성한다. 이렇게 미리 생성되는 쓰레드의 수는 풀의 최소 크기를 결정한다
2. **작업 할당**: 쓰레드 풀은 프로그램에서 실행해야 하는 작업을 받아들이고, 이를 쓰레드에 할당한다. 일반적으로 이 작업은 큐(queue)나 스택(stack) 같은 자료구조를 통해 관리된다.
3. **쓰레드 관리**: 쓰레드 풀은 풀의 현재 크기와 작업의 수를 기반으로 필요한 경우 추가적인 쓰레드를 생성하거나, 반대로 필요하지 않은 쓰레드를 종료할 수 있다. 이는 동적으로 시스템의 로드를 관리하는 데 도움이 된다.
4. **쓰레드 재사용**: 쓰레드 풀에서는 일단 작업이 완료된 쓰레드를 종료하지 않고, 다음 작업에 재사용한다. 이는 쓰레드 생성과 소멸에 대한 비용을 크게 줄일 수 있다.
## 직접 만들어보는 Thread Pool
```java
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
public class CustomThreadPool {
private final List<Thread> threads = new ArrayList<>();
private final Queue<Runnable> queue = new ConcurrentLinkedDeque<>();
public CustomThreadPool(int poolSize) {
for (int i = 0; i < poolSize; i++) {
Thread thread = new Thread(() -> {
while (true) {
try {
Runnable job = queue.poll();
if (job != null) {
job.run();
} else {
Thread.sleep(20);
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
threads.add(thread);
thread.start();
}
}
public void execute(Runnable task) {
queue.add(task);
}
public static void main(String[] args) {
CustomThreadPool pool = new CustomThreadPool(5);
for (int i = 0; i < 10; i++) {
int nowCount = i;
pool.execute(() -> System.out.println("Task " + nowCount + " is running."));
}
}
}
```
## ExecutorService

- 병렬 작업 시 여러개의 작업을 효율적으로 처리하기 위해 제공되는 자바 라이브러리
- 흔히 말하는 ThreadPool구현을 매우 용이하게 할 수 있으므로, Java에서 스레드 풀을 사용하고자 할 때 사용한다
- Runnable, Callable 중 하나를 상속하여 구현한 클래스를 인자로 받아서 처리할 수 있다.
- Runnable : return 값이 없는 스레드
- Callable : return 값이 있는 스레드
### ExecutorService의 종류
- `CachedThreadPool` : 스레드를 캐싱하는 스레드 풀. 필요할 때 필요한 만큼 스레드 풀을 생성한다. 이미 생성된 스레드를 재활용할 수 있기 때문에 성능상의 이점이 있을 수 있다. 다만 스레드 수가 폭발적으로 증가할 수 있다. (여기서 쓰이는 캐싱의 의미는 일정시간동안 스레드를 검사한다는 뜻이다. 60초동안 작업이 없으면 Pool에서 제거한다)
- `Executors.newCachedThreadPool()`
- `ScheduledThreadPool` : 일정 시간 뒤에 실행되는 작업이나 주기적으로 수행되는 작업에 사용된다.
- `Executors.newScheduledThreadPool()`
- `FixedThreadPool` : 고정된 개수를 가진 스레드풀
- `Executors.newFixedThreadPool()`
- `SingleThreadExecutor` : 한 개의 스레드로 작업을 처리하는 스레드풀. 스레드 풀이라기보단 TaskPool의 개념이 더 적합하다
- `Executors.newSingleThreadExecutor()`
### 메서드
- `submit()` : 작업을 추가할 땐 `submit` 메서드를 사용하면 된다
- `shutdown()` : 더 이상 쓰레드풀에 작업을 추가하지 못하도록 한다. 그리고 처리 중인 Task가 모두 완료되면 쓰레드풀을 종료시킨다.
- `awaitTermination()` : 이미 수행 중인 Task가 지정된 시간동안 끝나기를 기다리고 지정된 시간 내에 끝나지 않으면 false를 리턴하며, 이 때 `shutdownNow()`를 호출하면 실행 중인 Task를 모두 강제로 종료시킬 수 있다.
### ExecutorService 예시
#### 객체 생성
```java
ExecutorService executor = Executors.newFixedThreadPool(4);
```
#### 작업 추가
```java
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
System.out.println("Do Something");
return null;
});
```
#### Sample 1
- `submit( () -> {} )`은 멀티스레드로 처리할 작업을 예약한다. 인자로 람다식을 전달할 수 있다. (Runnable, Callable)
```java
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");
}
}
```
```log
Job1 pool-1-thread-1
Job3 pool-1-thread-1
Job4 pool-1-thread-1
Job2 pool-1-thread-2
end
```
#### Sample 2
- `newSingleThreadExecutor` 를 사용하면 순차적으로 처리된다.
```java
public class ExecutorServiceTest2 {
public static void main(String args[]) throws InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
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);
});
executor.shutdown();
executor.awaitTermination(20, TimeUnit.SECONDS);
System.out.println("end");
}
}
```
```log
Job1 pool-1-thread-1
Job2 pool-1-thread-1
Job3 pool-1-thread-1
Job4 pool-1-thread-1
end
```