- 작업: 추상적이면서 명확하게 구분된 업무의 단위
스레드에서 작업 실행
클라이언트의 요청 내용을 메인 스레드에서 직접 처리하지 않고, 클라이언트가 접속할 때마다 반복문에서 해당 클라이언트의 요청 처리르르 담당하는 새로운 스레드 매번 생성
- 작업을 처리하는 기능이 메인 스레드에서 떨어져 나오기 때문에 메인 반복문은 다음 클라이언트의 접속을 기다리는 부분으로 빨리 넘어갈 수 있음
- 동시에 여러 작업을 병렬로 처리할 수 있기 때문에 두 개 이상의 요청을 받아 동시에 처리할 수 있음
- 실제 작업을 처리하는 스레드의 프로그램은 여러 클라이언트가 접속하는 경우 동시에 동작할 가능성이 매우 높기 때문에 스레드 안전성을 확보해야 함
스레드를 많이 생성할 때의 문제점
- 스레드 라이프 사이클 문제: 스레드를 생성하고 제거하는 작업에도 자원이 소모된다. 스레드를 생성하고 제거하는 데 실제로 얼마만큼의 자원을 소모하는지는 운영체제에 따라 다르지만, 스레드를 생성하는 과정에는 일정량의 시간 필요하다. 클라이언트의 요청이 간단하면서 자주 발생하는 유형이라면 요청이 들어올 때마다 매번 새로운 스레드를 생성하는 일이 상대적으로 전체 작업에서 많은 부분을 차지할 수 있다.
- 자원 낭비: 실행 중인 스레드는 시스템의 자원, 특히 메모리를 소모한다. 하드웨어에 실제로 장착되어 있는 프로세서보다 많은 수의 스레드가 만들어져 동작 중이라면, 실제로는 대부분의 스레드가 대기 상태에 머무르며 대기 상태에 머무르는 스레드가 많아지면 많아질수록 많은 메모리가 필요로 하고, JVM의 가비지 콜렉터에 가해지는 부하가 늘어날 뿐 아니라 CPU를 사용하기 위해 여러 스레드가 경쟁하는 모양이 되기 때문에 메모리 이외에도 많은 자원 소모
- 안전성 문제: 모든 시스템에는 생성할 수 있는 스레드의 개수가 제한되어 있다. 몇 개까지 만들 수있는지는 플랫폼과 운영체제마다 다르고, JVM을 실행할 때 지정하는 인자나 Therad 클래스에 필요한 스택의 크기에 따라서 달라지기도 한다. 제한된 양을 모두 사용하고 나면 아마 OOM이 발생한다.
Executor 프레임웍
- 작업은 논리적인 업무의 단위이며, 스레드는 특정 작업을 비동기적으로 동작시킬 수 있는 방법 제공
- 스레드 풀: 스레드를 관리하는 측면에서 통제력을 갖출 수 있도록 해주며, java.util.concurrent 패키지에 있는 Executor 프레임웍의 일부부능로 유연하게 사용할 수 있는 스레드 풀이 만들어져 있음
public interface Executor {
void excute(Runnable commend);
}
- Executor 인터페이스는 단순해 보이지만 다양한 여러 종류의 작업 실행 정책을 지원하는 유연하면서도 강력한 비동기적 작업 실행 프레임웍의 근간을 이루는 인터페이스
- Executor는 작업 등록(task submission)과 작업 실행(task execution)을 분리하는 표준적인 방법이며, 각 작업은 Runnable 형태로 정의
- Executor 인터페이스를 구현한 클래스는 작업의라이프 사이클 관리 기능도 갖고 있고, 몇 가지 통계 값을 뽑거나 몇가지 작업 실행 과정을 관리하고 모니터링 하기 위한 기능도 가짐
- Executor의 구조는 프로듀서-컨슈머 패턴에 기반하고 있으며, 작업을 생성해 등록하는 클래스가 프로듀서(처리해야 할 작업을 생성하는 주체)가 되고 작업을 실제로 실행하는 스레드가 컨슈머(생성된 작업을 처리하는 주체)가 되는 모양을 갖추고 있음
Executors 클래스에 미리 정의되어 있는 스레드 풀
- newFixedThreadPool: 처리할 작업이 등록되면 그에 따라 실제 작업할 스레드를 하나씩 생성, 생성할 수 있는 스레드 최대 개수는 제한되어 있으며 제한된 개수까지 스레드를 생성하고 나면 더 이상 생성하지 않고 유지
- newCachedThreadPool: 캐시 스레드 풀은 현재 풀에 갖고 있는 스레드의 수가 처리할 작업의 수보다 많아서 쉬는 스레드가 많이 발생할 때 쉬는 스레드를 종료시켜 훨씬 유연하게 대응할 수 있으며, 처리할 작업의 수가 많아지면 필요한 만큼 스레드를 새로 생성. 스레드의 수에는 제한을 두지 않음
- newSingleThreadExecutor: 단일 스레드로 동작하는 Executor로서 작업을 처리하는 스레드가 단 하나뿐. 만약 작업 중 Exception이 발생해 비전상적으로 종료되면 새로운 스레드를 하나 생성해 나머지 작업 실행. 등록된 작업은 설정된 큐에 지정하는 순서(FIFO, LIFO, 우선순위)에 따라 반드시 순차적으로 처리
- newScheduledThreadPool: 일정 시간 이후에 실행하거나 주기적으로 작업을 실행할 수 있으며 스레드의 수가 고정되어 있는 형태의 Executor.Timer 클래스의 기능과 유사
Executor 동작 주기
- 작업을 비동기적으로 실행하기 때문에 앞서 실행시켰던 작업의 상태를 특정 시점에 정확하게 파악하기 어려움
- 애플리케이션을 종료하는 방법에는 안전한 종료 방법(graceful, 작업을 새로 등록하지는 못하고 시작된 모든 작업을 끝낼 때까지 기다림)이 있고, 강제적인 종료(abrupt, 플러그가 빠져 전원이 꺼지는 경우와 유사)방법
- 서비스를 실행하는 동작 주기와 관련해 Executor를 상속받은 ExecutorService 인터페이스에는 동작 주를 관리할 수 있는 여러 메서드가 추가
- ExecutorService가 갖고 있는 동작 주기에는 실행 중(running), 종료 중(shutting down), 종료(terminated)의 세 가지 형태가 있음
- ExecutorService를 처음 생성 했을 때에는 실행 중 상태로 동작하다가 shutdown() 메서드를 시행하면 안전한 종료 절차를 진행하며 종료 중 상태로 들어가며 이 상태에서는 새로운 작업을 등록받지 않으며, 이전에 등록되어 있던 작업까지는 모두 끝마침
- shutdownNow() 메서드를 실행하면 강제 종료 절차를 진행하여 현재 진행 중인 작업도 가능한 한 취소시키고, 실행되지 않고 대기 중이던 작업은 더 이상 실행시키지 않음
- 종료 절차가 시작된 이후 실행 중이거나 대기 중이던 작업을 모두 끝내고 나면 ExecutorService는 종료 상태로 들어가고, 종료 상태로 들어갈 때까지 기다리고자 한다면 awaitTermination()메서드로 대기할 수도 있고 isTerminated 메서드를 주기적으로 호출해 종료 상태로 들어갔는지 확인할 수도 있음
지연 작업, 주기적 작업
- Timer 클래스는 상대 시각만 지원할 뿐만 아니라 절대 시각도 지원한다. 따라서 절대 시각을 사용하는 경우 시스템 하드웨어의 시각을 변경시키면 Timer에 스케줄된 작업도 함께 변경되는 약간의 단점이 있기 때문에 가능하면 ScheduledThreadPoolExecutor를 사용하는 방법을 생각해보는게 좋음
- ScheduledThreadPoolExecutor를 생성하려면 직접 클래스의 생성 메서드를 호출해 생성하는 방법이 있고, 아니면 newScheduledThreadPool 팩토리 메서드를 사용해 생성하는 방법이 있음
- Timer 클래스는 등록된 작업을 실행시키는 스레드 하나만 생성해 사용하여 Timer 에 등록된 특정 작업이 너무 오래 실행된다면 등록된 다른 TimerTask 작업이 예정된 시각에 실행되지 못할 가능성이 있음
- TimerTask가 동작하던 도중에 예상치 못한 Exception을 던져버리는 경우에 예상치 못한 상태로 넘어갈 수 있음
- 특별한 스케줄 방법을 지웧나는 스케줄링 서비스를 구현해야 할 필요가 있다면, BlockingQueue를 구현하면서 ScheduledThreadPoolExecutor와 비슷한 기능ㅇ을 제공하는 DelayQueue 클래스를 사용해 보는 것이 좋다. DelayQueue는 큐 내부 여러 개의 Delayed 객체로 작업을 관리하며, 각각의 Delayed 객체는 저마다의 시각을 갖고 있음
- DelayedQueue를 사용하면 Delayed 내부의 시각이 만료된 객체만 take 메서드로 가져갈 수 있음
Runnable
- run 메서드는 실행이 끝난 다음 뭔가 결과 값을 리턴해 줄 수도 없고, 예외가 발생할 수 있다고 throws 구문으로 표현할 수도 없음
- 결과를 얻는 데 시간이 걸리는 기능은 Runnable 대신 Callable을 사용하는게 좋음
Callable
- Callable 인터페이스에서는 핵심 메서드인 call을 실행하고 나면 결과 값을 돌려받을 수 있으며, Exception도 발생시킬 수 있도록 되어 있음
- Executor에는 Callable 뿐만 아니라 Runnable이나 java.security.PrivilegedAction등 여러 가지 유형의 작업을 실행할 수 있는 기능이 들어 있음
- Runnable과 Callable 모두 어떤 작업을 추상화하기 위한 도구
- 작업은 일반적으로 유한한 성격을 가져 시작하는 지점이 명확하고 언젠가는 작업이 끝나게 되어있다. Executor에서 실행한 작업은 생성(created), 등록(submitted), 실행(started), 종료(completed)와 같은 네 가지의 상태를 통과
Future
- Future는 특정 작업이 정상적으로 완료됐는지, 아니면 취소됐는지 등에 대한 정보를 확인할 수 있도록 만들어진 클래스
- Future가 동작하는 사이클에서 한 번 지나간 상태는 되돌릴 수 없다는 점을 염두에 둬야 하며, 이렇게 사이클을 되돌릴 수 없다는 것은 ExecutorService와 동일
- get메서드는 작업이 진행되는 상태에 따라 다른 유형으로 동작
— 작업이 완료 상태에 들어가 있다면 즉시 결과 값을 리턴하거나 Exception 발생
— 아직 작업을 시작하지 않았거나 작업이 실행되고 있는 상태라면 작업이 완료될 때까지 대기
— 작업 실행이 모두 끝난 상태에서 Exception이 발생했었다면 get 메서드는 원래 발생했던 Exception 을 ExecutionException이라는 예외 클래스에 담아 던지며, 이 경우 원래 발생한 예외는 getCause() 메서드로 확인 가능
— 작업이 중간에 취소됐다면 get메서드에서 CancellationException이 발생 - ExecutionService 클래스의 submit 메서드는 모두 Future 인스턴스를 리턴하므로 Executor에 Runnable이나 Callable을 등록하면 Future 인스턴스를 받을 수 있고, 받은 Future 인스턴스를 사용해 작업의 결과를 확인하거나 실행 도중에 작업을 취소할 수도 있음, 아니면 Runnable이나 Callable을 사용해 직접 FutureTask 인스턴스를 생성하는 방법도 있음
CompletionService
- Executor의 기능과 BlockingQueue의 기능을 하나로 모은 인터페이스
- 필요한 Callable 작업을 등록해 실행시킬 수 있고, take나 poll과 같은 큐 메서드를 사용해 작업이 완료되는 순간 온료된 작업의 Future 인스턴스를 받아올 수 있음
- CompletionService를 구현한 클래스로는 ExecutorCompletionService가 있는데, 등록된 작업은 Executor를 통해 실 행
- 여러 개의 ExecutorCompletionService에서 동일한 Executor를 공유해 사용할 수도 있으며 실행을 맡은 Executor는 하나만 두고 동일한 작업을 처리하는 여러가지 ExecutorCompletionService를 생성해 사용하는 일도 가능
작업 실행 시간 제한
- 타임아웃을 지정할 수 있는 Future.get 메서드를 사용하면 시간 제한 요구사항을 만족할 수 있음
> 지정한 시간이 지나도 결과를 만들어내지 못하면 TimeoutExceptioon을 던지면서 실행이 멈추게 되어 있음