본문은 Effective Java를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
선결론
- 스레드를 직접 관리하면서 작업을 수행하기보다는 이미 제공된 기술들을 적극적으로 활용하자
- 작업 큐를 손수 만들거나 스레드를 직접 다루는 일은 지양하자
실행자 프레임워크의 구성요소
- 자바는 비동기/병렬 처리를 위해 java.util.concurrent에서 실행자 프레임워크(Executor Framework)를 제공한다
- 실행자 프레임워크를 사용하면 스레드의 생성과 재사용을 쉽게 할 수 있기 때문에 Runnable 객체를 처리하는 데 유용하다
Executor 인터페이스
- 넘겨받은 Runnable 태스크를 실행하는 역할을 한다
public interface Executor {
void execute(Runnable command);
}
- Excecutor 인터페이스를 사용하면 작업 단위와 실행 메커니즘이 분리된다
// 스레드를 직접 다루는 방식: 스레드가 작업 단위와 실행 메커니즘 역할을 모두 수행
// 쓰레드와 작업이 1:1
new Thread(new(RunnableTask1())).start()
new Thread(new(RunnableTask2())).start()
// Executor 활용: 작업 단위와 실행 메커니즘이 분리됨
// 쓰레드와 작업이 M:N, 프로그래머는 작업을 실행시키고 내부에서 적절하게 처리됨
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...
ExecutorService 인터페이스
- Executor를 확장한 인터페이스로서 종료를 수행하는 메서드들을 제공하고 비동기 작업의 경과를 추적하는 Future 객체를 생성할 수 있다
- 다음은 ExecutorService의 주요 기능들이다
- 특정 태스크가 완료되기를 기다린다(get)
- 태스크 모음 중 아무 것 하나가 완료되기를 기다린다(invokeAny)
- 모든 태스크가 완료되기를 기다린다(awaitTermination)
- 실행자 서비스가 종료하기를 기다린다(awaitTermination 메서드)
- 완료된 태스크들의 결과를 차례로 받는다(ExecutorCompletionService 이용)
- 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다(ScheduledThreadPoolExecutor 이용)
Executors 클래스
- Excutors는 쓰레드풀 생성을 할 수 있는 다양한 정적 팩터리 메서드를 제공한다
- newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool 등
- newCachedThradPool은 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임하게 되는데, 이 과정에서 스레드가 계속해서 생성되어 서버에 부하를 줄 수 있다
- Executors 대신 ThreadPoolExecutor를 직접 사용하여 스레드 풀을 만들 수 있지만, 이는 스레드 풀 동작을 결정하기 위한 속성들을 일일히 세팅해줘야 하므로 번거롭다
- 실제로 Executors 내부의 정적 팩터리 메서드들 중 몇몇은 ThreadPoolExecutor에 속성을 세팅한 후 반환한다
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
fork-join 태스크
- 자바 7부터 실행자 프레임워크는 포크-조인(fork-join) 태스크를 지원한다
- fork-join 태스크는 ForkJoinPool이라는 ExecutorService가 실행해준다(이 실행자 서비스의 생성 또한 Executors 클래스에서 지원해준다)
- fork-join 태스크(ForkJoinTask의 인스턴스)는 작은 하위 태스크로 나뉠 수 있고, ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리하며, 일을 먼저 끝내는 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다
- 즉, CPU를 최대한 활용한다
- ForkJoinPool을 이용해 만든 병렬 스트림(아이템 48)을 이용하면 이러한 작업을 쉽게 수행할 수 있다
스레드풀 동작원리
- 태스크가 들어오면 스레드가 바로 태스크를 수행하는 것이 아니라 작업 큐에 담는다
- 태스크에는 Runnable과 Callable이 존재한다
- Callable은 Runnable과 유사하지만 값을 반환하고 임의의 예외를 던질 수 있다
- 스레드풀에 존재하는 스레드 중 작업을 마친 스레드들이 작업 큐에 담긴 태스크를 수행한다
- 링크를 통해 위 동작을 구현하는 과정을 확인할 수 있다
'책 > Effective Java' 카테고리의 다른 글
[이펙티브 자바] 아이템 88: readObject 메서드는 방어적으로 작성하라 (0) | 2022.07.28 |
---|---|
[이펙티브 자바] 아이템 72: 표준 예외를 사용하라 (0) | 2022.07.16 |
[이펙티브 자바] 아이템 71: 필요 없는 검사 예외 사용은 피하라 (0) | 2022.07.16 |
[이펙티브 자바] 아이템 70: 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라 (0) | 2022.07.15 |
[이펙티브 자바] 아이템 69: 예외는 진짜 예외 상황에만 사용하라 (0) | 2022.07.15 |