핵심 내용
- 스레드를 직접 다루는 대신 실행자 프레임워크와 태스크를 활용하고, 병렬 처리는 스트림을 통해 진행하자.
- 이렇게 하면 작업과 실행 메커니즘이 분리되며, 실행자는 태스크의 수행 정책을 선택하게 해주며, 병렬 처리를 효과적으로 수행할 수 있다.
java.util.concurrent 패키지
: 실행자 프레임워크(Executor Framework)
- 해당 패키지가 나오기 이전에는 작업 큐를 사용하려면 안전 실패나 응답 불가능한 문제를 해결하기 위한 코드를 추가해야 했다.
- java.util.concurrent 패키지가 등장하면서 실행자 프레임워크(Executor Framework)를 제공한다.
// 작업 큐를 생성하다. (ExecutorService는 실행자 서비스를 나타내는 인터페이스)
ExcutorService exec = Executors.newSingleThreadExcutor();
// Excutor에 실행할 태스크(task; 작업)를 넘기는 방법이다.
exec.execute(runnable);
// Excutor를 우아하게 종료시키는 방법이다(이 작업이 실패하면 VM 자체가 종료되지 않을 것이다)
exec.shutdown();
- 실행자 서비스의 주요 기능
- 특정 태스크가 완료되기를 기다린다.(get 메서드)
- 태스크 모음 중 아무것 하나(invokeAny 메서드) 혹은 모든 태스크(invokeALL 메서드)가 완료되기를 기다린다.
- 실행자 서비스가 종료하기를 기다린다(awaitTermination 메서드)
- 완료된 태스크들의 결과를 차례로 받는다(ExecutorCompletionService 이용)
- 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다(ScheculedThread PoolExecutor 이용)
스레드 풀
- 스레드 풀은 스레드 관리를 효율적으로 처리하여 작업을 동시에 처리하도록 돕는다.
- java.util.concurrent.Excutors의 정적 팩토리 메소드를 사용하여 원하는 스레드 풀을 생성할 수 있다.
- ex: Executors.newFixedThreadPool(int nThreads)는 스레드 개수를 고정한 스레드 풀을 생성한다.
- 생성한 스레드 풀의 스레드 개수는 정적 팩토리 메서드에 전달하는 인자나 스레드 풀의 설정을 통해 조절할 수 있다.
- 스레드 개수를 고정하거나 동적으로 관리하여 효율적으로 작업을 분배할 수 있습니다.
- 만약 특별한 설정이나 동작을 원한다면 ThreadPoolExecutor 클래스를 사용할 수 있습니다.
- 스레드 풀의 동작을 거의 모든 측면에서 설정하고 관리할 수 있는 클래스이다.
- 스레드 풀의 크기, 작업 큐의 종류, 스레드 생성, 스레드 종료 규칙 등을 세밀하게 제어할 수 있다.
Executors.newCachedThreadPool
🌈 작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool을 사용하고, 무거운 프로덕션 서버라면 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나 완전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하자.
- Executors.newCachedThreadPool()은 요청받은 작업들이 큐에 쌓이지 않고 즉시 실행된다.
- 만약 현재 사용 가능한 스레드가 없다면 새로운 스레드를 생성하여 작업을 처리하려고 한다.
- 따라서 많은 작업이 동시에 처리되려고 시도하니 CPU 이용률이 치솟는다.
- 즉, 너무 많은 스레드가 생성되고 CPU가 과다하게 사용되면서 성능이 저하된다.
Runnable와 Callable
: 실행자 프레임워크
🌈 작업 큐를 손수 만드는 일은 삼가야 하고, 스레드를 직접 다루는 것도 일반적으로 삼가야 한다.
- 유지 보수와 확장이 어려울 수 있으며, 스레드 관리에 대한 복잡성과 위험이 증가할 수 있다.
- 실행자 프레임워크는 task를 알아서 관리하고 스레드에 할당해준다. 즉, 작업과 스레드 관리를 분리할 수 있다.
- task는 Runnable과 Callable이라는 두 가지 종류가 있다.
- cf. Runnable과 달리 Callable은 값을 반환하거나 예외를 던질 수 있다.
- 실행자 서비스는 이러한 task를 실행하는 역할을 한다.
- 즉, 서비스에 작업을 맡기면 프레임워크가 작업을 스레드에 할당하고, 작업이 완료되면 결과를 돌려준다.
- 또한, 원하는 작업 수행 정책을 선택하고 변경할 수 있다.
- ex. 스레드 할당 방식
포크-조인(fork-join)
: 실행자 프레임워크
- 포크-조인 풀은 특별한 종류의 실행자 서비스로서, 포크-조인 태스크들을 처리하는 역할을 한다.
- 포크-조인 태스크는 ForkJoinTask 클래스의 인스터스로 나타내며, 이는작은 하위 태스크로 분할 가능하다.
- 이러한 하위 태스크들은 포크-조인 풀에 속한 스레드에 의해 병렬로 처리된다.
- 한 스레드가 일을 먼저 끝내고 다른 스레드의 남은 태스크를 가져와 처리할 수 있어서 CPU 자원을 효율적으로 활용한다.
- 포크-조인 태스크를 직접 잘 작성하는 것은 어려우나, 포크-조인 풀을 활용하여 병렬 스트림(item 48)을 만들면 쉽게 사용할 수 있다.
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 82. 스레드 안전성 수준을 문서화하라. (0) | 2023.08.13 |
---|---|
[effective java] 아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라. (0) | 2023.08.13 |
[effective java] 아이템 79. 과도한 동기화는 피하라. (0) | 2023.08.13 |
[effective java] 아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라. (0) | 2023.08.13 |
[effective java] 아이템 77. 예외를 무시하지 말라. (0) | 2023.08.12 |